Skip to content

Commit c24d22d

Browse files
feature (fastboot): parallelize the loading of assets
1 parent 9ad8ec1 commit c24d22d

File tree

3 files changed

+96
-63
lines changed

3 files changed

+96
-63
lines changed

public/assets/boot/bundler_init.js

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,35 @@ window.bundler = (function(origin) {
22
const esModules = {};
33
return {
44
register: (path, code) => {
5+
const fullpath = origin + path;
56
if (path.endsWith(".js")) {
67
code = code.replace(/from\s?"([^"]+)"/g, (_, spec) =>
7-
`from "${new URL(spec, origin + path).href}"`,
8+
`from "${new URL(spec, fullpath).href}"`,
89
);
910
code = code.replace(/\bimport\s+"([^"]+)"/g, (_, spec) =>
10-
`import "${new URL(spec, origin + path).href}"`,
11+
`import "${new URL(spec, fullpath).href}"`,
12+
);
13+
code = code.replace(
14+
/(?<!["])\bimport\.meta\.url\b(?!["])/g,
15+
`"${fullpath}"`,
16+
);
17+
esModules[fullpath] = "data:text/javascript," + encodeURIComponent(
18+
code + `\n//# sourceURL=${path}`,
1119
);
12-
code = code.replace(/(?<!["])\bimport\.meta\.url\b(?!["])/g, `"${origin + path}"`);
13-
code += `\n//# sourceURL=${path}`;
14-
esModules[origin + path] = "data:text/javascript," + encodeURIComponent(code);
1520
} else if (path.endsWith(".css")) {
1621
code = code.replace(/@import url\("([^"]+)"\);/g, (m, rel) => {
17-
const $style = document.head.querySelector(`style[id="${new URL(rel, origin + path).href}"]`);
22+
const $style = document.head.querySelector(
23+
`style[id="${new URL(rel, fullpath).href}"]`
24+
);
1825
if (!$style) throw new DOMException(
1926
`Missing CSS dependency: ${rel} (referenced from ${path})`,
2027
"NotFoundError",
2128
);
2229
return `/* ${m} */`;
2330
});
24-
code += `\n/*# sourceURL=${path} */`;
2531
document.head.appendChild(Object.assign(document.createElement("style"), {
26-
innerHTML: code,
27-
id: origin + path,
32+
innerHTML: code + `\n/*# sourceURL=${path} */`,
33+
id: fullpath,
2834
}));
2935
}
3036
},

public/index.frontoffice.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@
3939
try {
4040
if (!HTMLScriptElement.supports?.("importmap")) throw new Error("fastboot is not supported on this platform");
4141
{{ load_asset "assets/boot/bundler_init.js" }}
42-
await new Promise((resolve, reject) => document.head.appendChild(Object.assign(document.createElement("script"), {
42+
await Promise.all(Array({{ .bundle_size }}).fill().map((_, i) => new Promise((resolve, reject) => document.head.appendChild(Object.assign(document.createElement("script"), {
4343
type: "module",
44-
src: `./assets/bundle.js?version=${window.VERSION}`,
44+
src: `./assets/bundle.js?version=${window.VERSION}&chunk=${i+1}`,
4545
onload: resolve,
4646
onerror: reject,
47-
})));
47+
})))));
4848
{{ load_asset "assets/boot/bundler_complete.js" }}
4949
} catch (err) { console.error(err); }
5050

server/ctrl/static.go

Lines changed: 78 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"os"
1414
"path/filepath"
1515
"regexp"
16+
"strconv"
1617
"strings"
1718
"text/template"
1819

@@ -233,11 +234,12 @@ func ServeIndex(indexPath string) func(*App, http.ResponseWriter, *http.Request)
233234
sign := signature()
234235
base := WithBase("/")
235236
templateData := map[string]any{
236-
"base": base,
237-
"version": BUILD_REF,
238-
"license": LICENSE,
239-
"hash": sign,
240-
"favicon": favicon(),
237+
"base": base,
238+
"version": BUILD_REF,
239+
"license": LICENSE,
240+
"hash": sign,
241+
"favicon": favicon(),
242+
"bundle_size": len(preload),
241243
}
242244
calculatedEtag := QuickHash(base+BUILD_REF+LICENSE+sign, 10)
243245
head.Set("ETag", calculatedEtag)
@@ -258,8 +260,8 @@ func ServeIndex(indexPath string) func(*App, http.ResponseWriter, *http.Request)
258260
}
259261
}
260262

261-
func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
262-
paths := []string{
263+
var preload = [][]string{
264+
{
263265
"/assets/" + BUILD_REF + "/boot/ctrl_boot_frontoffice.js",
264266
"/assets/" + BUILD_REF + "/boot/router_frontoffice.js",
265267
"/assets/" + BUILD_REF + "/boot/common.js",
@@ -303,6 +305,9 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
303305
"/assets/" + BUILD_REF + "/helpers/sdk.js",
304306

305307
"/assets/" + BUILD_REF + "/lib/rx.js",
308+
"/assets/" + BUILD_REF + "/lib/ajax.js",
309+
},
310+
{
306311
"/assets/" + BUILD_REF + "/lib/vendor/rxjs/rxjs.min.js",
307312
"/assets/" + BUILD_REF + "/lib/vendor/rxjs/rxjs-ajax.min.js",
308313
"/assets/" + BUILD_REF + "/lib/vendor/rxjs/rxjs-shared.min.js",
@@ -311,7 +316,6 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
311316
"/assets/" + BUILD_REF + "/lib/path.js",
312317
"/assets/" + BUILD_REF + "/lib/random.js",
313318
"/assets/" + BUILD_REF + "/lib/settings.js",
314-
"/assets/" + BUILD_REF + "/lib/ajax.js",
315319
"/assets/" + BUILD_REF + "/lib/animate.js",
316320
"/assets/" + BUILD_REF + "/lib/assert.js",
317321
"/assets/" + BUILD_REF + "/lib/dom.js",
@@ -321,16 +325,16 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
321325
"/assets/" + BUILD_REF + "/lib/error.js",
322326

323327
"/assets/" + BUILD_REF + "/locales/index.js",
324-
325328
"/assets/" + BUILD_REF + "/model/config.js",
326329
"/assets/" + BUILD_REF + "/model/chromecast.js",
327330
"/assets/" + BUILD_REF + "/model/session.js",
328331
"/assets/" + BUILD_REF + "/model/plugin.js",
329332

330333
"/assets/" + BUILD_REF + "/pages/ctrl_logout.js",
331334
"/assets/" + BUILD_REF + "/pages/ctrl_error.js",
335+
},
336+
{
332337
"/assets/" + BUILD_REF + "/pages/ctrl_homepage.js",
333-
334338
"/assets/" + BUILD_REF + "/pages/ctrl_connectpage.js",
335339
"/assets/" + BUILD_REF + "/pages/ctrl_connectpage.css",
336340
"/assets/" + BUILD_REF + "/pages/connectpage/ctrl_form.css",
@@ -341,18 +345,24 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
341345
"/assets/" + BUILD_REF + "/pages/connectpage/model_config.js",
342346
"/assets/" + BUILD_REF + "/pages/connectpage/ctrl_form_state.js",
343347

348+
"/assets/" + BUILD_REF + "/pages/filespage/thing.js",
349+
"/assets/" + BUILD_REF + "/pages/filespage/thing.css",
350+
},
351+
{
344352
"/assets/" + BUILD_REF + "/pages/ctrl_filespage.js",
345353
"/assets/" + BUILD_REF + "/pages/ctrl_filespage.css",
346-
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_submenu.css",
347354
"/assets/" + BUILD_REF + "/pages/filespage/model_acl.js",
348355
"/assets/" + BUILD_REF + "/pages/filespage/cache.js",
349-
"/assets/" + BUILD_REF + "/pages/filespage/thing.js",
350-
"/assets/" + BUILD_REF + "/pages/filespage/thing.css",
351356
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_filesystem.js",
352357
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_filesystem.css",
353358
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_upload.js",
354359
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_upload.css",
360+
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_newitem.js",
361+
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_newitem.css",
362+
},
363+
{
355364
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_submenu.js",
365+
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_submenu.css",
356366
"/assets/" + BUILD_REF + "/pages/filespage/state_config.js",
357367
"/assets/" + BUILD_REF + "/pages/filespage/helper.js",
358368
"/assets/" + BUILD_REF + "/pages/filespage/model_files.js",
@@ -367,8 +377,6 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
367377
"/assets/" + BUILD_REF + "/pages/filespage/modal_delete.js",
368378
"/assets/" + BUILD_REF + "/pages/filespage/state_selection.js",
369379
"/assets/" + BUILD_REF + "/pages/filespage/state_newthing.js",
370-
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_newitem.js",
371-
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_newitem.css",
372380

373381
// "/assets/" + BUILD_REF + "/pages/ctrl_viewerpage.js", // TODO: dynamic imports
374382
"/assets/" + BUILD_REF + "/pages/ctrl_viewerpage.css",
@@ -379,66 +387,85 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
379387
"/assets/" + BUILD_REF + "/pages/viewerpage/application_downloader.css",
380388
"/assets/" + BUILD_REF + "/pages/viewerpage/component_menubar.js",
381389
"/assets/" + BUILD_REF + "/pages/viewerpage/component_menubar.css",
382-
}
383-
384-
var isDebug = os.Getenv("DEBUG") == "true"
390+
},
391+
}
385392

386-
build := func(quality int) (bundlePlain []byte, bundleBr []byte, etag string) {
387-
var buf bytes.Buffer
388-
for _, path := range paths {
389-
curPath := "/assets/" + strings.TrimPrefix(path, "/assets/"+BUILD_REF+"/")
390-
f := applyPatch(curPath)
391-
if f == nil {
392-
file, err := WWWPublic.Open(curPath)
393-
if err != nil {
394-
Log.Warning("static::bundler failed to find file %s", err.Error())
395-
continue
393+
func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
394+
isDebug := os.Getenv("DEBUG") == "true"
395+
buildChunks := func(quality int) (chunks [][]byte, chunksBr [][]byte, etags []string) {
396+
numChunks := len(preload)
397+
chunks = make([][]byte, numChunks+1)
398+
chunksBr = make([][]byte, numChunks+1)
399+
etags = make([]string, numChunks+1)
400+
var fullBuf bytes.Buffer
401+
for i := 0; i < numChunks; i++ {
402+
var chunkBuf bytes.Buffer
403+
for _, path := range preload[i] {
404+
curPath := "/assets/" + strings.TrimPrefix(path, "/assets/"+BUILD_REF+"/")
405+
f := applyPatch(curPath)
406+
if f == nil {
407+
file, err := WWWPublic.Open(curPath)
408+
if err != nil {
409+
Log.Warning("static::bundler failed to find file %s", err.Error())
410+
continue
411+
}
412+
f = new(bytes.Buffer)
413+
if _, err := io.Copy(f, file); err != nil {
414+
Log.Warning("static::bundler msg=copy_error err=%s", err.Error())
415+
continue
416+
}
417+
file.Close()
396418
}
397-
f = new(bytes.Buffer)
398-
if _, err := io.Copy(f, file); err != nil {
399-
Log.Warning("static::bundler msg=copy_error err=%s", err.Error())
419+
code, err := json.Marshal(f.String())
420+
if err != nil {
421+
Log.Warning("static::bundle msg=marshal_failed path=%s err=%s", path, err.Error())
400422
continue
401423
}
402-
file.Close()
424+
line := fmt.Sprintf("bundler.register(%q, %s);\n", WithBase(path), code)
425+
chunkBuf.WriteString(line)
426+
fullBuf.WriteString(line)
403427
}
404-
code, err := json.Marshal(f.String())
405-
if err != nil {
406-
Log.Warning("static::bundle msg=marshal_failed path=%s err=%s", path, err.Error())
407-
continue
408-
}
409-
fmt.Fprintf(&buf, "bundler.register(%q, %s);\n", WithBase(path), code)
428+
chunks[i+1] = chunkBuf.Bytes()
429+
chunksBr[i+1], _ = cbrotli.Encode(chunks[i+1], cbrotli.WriterOptions{Quality: quality})
430+
etags[i+1] = QuickHash(string(chunks[i+1]), 10)
410431
}
411-
etag = QuickHash(string(bundlePlain), 10)
412-
bundlePlain = buf.Bytes()
413-
if quality > 0 {
414-
bundleBr, _ = cbrotli.Encode(bundlePlain, cbrotli.WriterOptions{Quality: quality})
415-
}
416-
return bundlePlain, bundleBr, etag
432+
chunks[0] = fullBuf.Bytes()
433+
chunksBr[0], _ = cbrotli.Encode(chunks[0], cbrotli.WriterOptions{Quality: quality})
434+
etags[0] = QuickHash(string(chunks[0]), 10)
435+
return chunks, chunksBr, etags
417436
}
418437

419438
quality := 11
420439
if isDebug {
421440
quality = 8
422441
}
423-
bundlePlain, bundleBr, etag := build(quality)
442+
chunks, chunksBr, etags := buildChunks(quality)
424443

425444
return func(ctx *App, res http.ResponseWriter, req *http.Request) {
426445
if isDebug {
427-
bundlePlain, bundleBr, etag = build(quality)
446+
chunks, chunksBr, etags = buildChunks(quality)
447+
}
448+
chunkIndex := 0
449+
if parsed, err := strconv.Atoi(req.URL.Query().Get("chunk")); err == nil {
450+
chunkIndex = parsed
451+
}
452+
if chunkIndex >= len(chunks) {
453+
http.NotFound(res, req)
454+
return
428455
}
429456
head := res.Header()
430457
head.Set("Content-Type", "application/javascript")
431458
head.Set("Cache-Control", "no-cache")
432-
head.Set("Etag", etag)
433-
if req.Header.Get("If-None-Match") == etag && etag != "" {
459+
head.Set("Etag", etags[chunkIndex])
460+
if req.Header.Get("If-None-Match") == etags[chunkIndex] && etags[chunkIndex] != "" {
434461
res.WriteHeader(http.StatusNotModified)
435462
return
436-
} else if strings.Contains(req.Header.Get("Accept-Encoding"), "br") && len(bundleBr) > 0 {
463+
} else if strings.Contains(req.Header.Get("Accept-Encoding"), "br") && len(chunksBr[chunkIndex]) > 0 {
437464
head.Set("Content-Encoding", "br")
438-
res.Write(bundleBr)
465+
res.Write(chunksBr[chunkIndex])
439466
return
440467
}
441-
res.Write(bundlePlain)
468+
res.Write(chunks[chunkIndex])
442469
}
443470
}
444471

0 commit comments

Comments
 (0)