feature (fastboot): parallelize the loading of assets

This commit is contained in:
MickaelK 2025-09-05 22:42:53 +10:00
parent 9ad8ec1f87
commit c24d22d650
3 changed files with 98 additions and 65 deletions

View file

@ -2,29 +2,35 @@ window.bundler = (function(origin) {
const esModules = {}; const esModules = {};
return { return {
register: (path, code) => { register: (path, code) => {
const fullpath = origin + path;
if (path.endsWith(".js")) { if (path.endsWith(".js")) {
code = code.replace(/from\s?"([^"]+)"/g, (_, spec) => code = code.replace(/from\s?"([^"]+)"/g, (_, spec) =>
`from "${new URL(spec, origin + path).href}"`, `from "${new URL(spec, fullpath).href}"`,
); );
code = code.replace(/\bimport\s+"([^"]+)"/g, (_, spec) => code = code.replace(/\bimport\s+"([^"]+)"/g, (_, spec) =>
`import "${new URL(spec, origin + path).href}"`, `import "${new URL(spec, fullpath).href}"`,
);
code = code.replace(
/(?<!["])\bimport\.meta\.url\b(?!["])/g,
`"${fullpath}"`,
);
esModules[fullpath] = "data:text/javascript," + encodeURIComponent(
code + `\n//# sourceURL=${path}`,
); );
code = code.replace(/(?<!["])\bimport\.meta\.url\b(?!["])/g, `"${origin + path}"`);
code += `\n//# sourceURL=${path}`;
esModules[origin + path] = "data:text/javascript," + encodeURIComponent(code);
} else if (path.endsWith(".css")) { } else if (path.endsWith(".css")) {
code = code.replace(/@import url\("([^"]+)"\);/g, (m, rel) => { code = code.replace(/@import url\("([^"]+)"\);/g, (m, rel) => {
const $style = document.head.querySelector(`style[id="${new URL(rel, origin + path).href}"]`); const $style = document.head.querySelector(
`style[id="${new URL(rel, fullpath).href}"]`
);
if (!$style) throw new DOMException( if (!$style) throw new DOMException(
`Missing CSS dependency: ${rel} (referenced from ${path})`, `Missing CSS dependency: ${rel} (referenced from ${path})`,
"NotFoundError", "NotFoundError",
); );
return `/* ${m} */`; return `/* ${m} */`;
}); });
code += `\n/*# sourceURL=${path} */`;
document.head.appendChild(Object.assign(document.createElement("style"), { document.head.appendChild(Object.assign(document.createElement("style"), {
innerHTML: code, innerHTML: code + `\n/*# sourceURL=${path} */`,
id: origin + path, id: fullpath,
})); }));
} }
}, },

View file

@ -39,12 +39,12 @@
try { try {
if (!HTMLScriptElement.supports?.("importmap")) throw new Error("fastboot is not supported on this platform"); if (!HTMLScriptElement.supports?.("importmap")) throw new Error("fastboot is not supported on this platform");
{{ load_asset "assets/boot/bundler_init.js" }} {{ load_asset "assets/boot/bundler_init.js" }}
await new Promise((resolve, reject) => document.head.appendChild(Object.assign(document.createElement("script"), { await Promise.all(Array({{ .bundle_size }}).fill().map((_, i) => new Promise((resolve, reject) => document.head.appendChild(Object.assign(document.createElement("script"), {
type: "module", type: "module",
src: `./assets/bundle.js?version=${window.VERSION}`, src: `./assets/bundle.js?version=${window.VERSION}&chunk=${i+1}`,
onload: resolve, onload: resolve,
onerror: reject, onerror: reject,
}))); })))));
{{ load_asset "assets/boot/bundler_complete.js" }} {{ load_asset "assets/boot/bundler_complete.js" }}
} catch (err) { console.error(err); } } catch (err) { console.error(err); }

View file

@ -13,6 +13,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strconv"
"strings" "strings"
"text/template" "text/template"
@ -238,6 +239,7 @@ func ServeIndex(indexPath string) func(*App, http.ResponseWriter, *http.Request)
"license": LICENSE, "license": LICENSE,
"hash": sign, "hash": sign,
"favicon": favicon(), "favicon": favicon(),
"bundle_size": len(preload),
} }
calculatedEtag := QuickHash(base+BUILD_REF+LICENSE+sign, 10) calculatedEtag := QuickHash(base+BUILD_REF+LICENSE+sign, 10)
head.Set("ETag", calculatedEtag) head.Set("ETag", calculatedEtag)
@ -258,8 +260,8 @@ func ServeIndex(indexPath string) func(*App, http.ResponseWriter, *http.Request)
} }
} }
func ServeBundle() func(*App, http.ResponseWriter, *http.Request) { var preload = [][]string{
paths := []string{ {
"/assets/" + BUILD_REF + "/boot/ctrl_boot_frontoffice.js", "/assets/" + BUILD_REF + "/boot/ctrl_boot_frontoffice.js",
"/assets/" + BUILD_REF + "/boot/router_frontoffice.js", "/assets/" + BUILD_REF + "/boot/router_frontoffice.js",
"/assets/" + BUILD_REF + "/boot/common.js", "/assets/" + BUILD_REF + "/boot/common.js",
@ -303,6 +305,9 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
"/assets/" + BUILD_REF + "/helpers/sdk.js", "/assets/" + BUILD_REF + "/helpers/sdk.js",
"/assets/" + BUILD_REF + "/lib/rx.js", "/assets/" + BUILD_REF + "/lib/rx.js",
"/assets/" + BUILD_REF + "/lib/ajax.js",
},
{
"/assets/" + BUILD_REF + "/lib/vendor/rxjs/rxjs.min.js", "/assets/" + BUILD_REF + "/lib/vendor/rxjs/rxjs.min.js",
"/assets/" + BUILD_REF + "/lib/vendor/rxjs/rxjs-ajax.min.js", "/assets/" + BUILD_REF + "/lib/vendor/rxjs/rxjs-ajax.min.js",
"/assets/" + BUILD_REF + "/lib/vendor/rxjs/rxjs-shared.min.js", "/assets/" + BUILD_REF + "/lib/vendor/rxjs/rxjs-shared.min.js",
@ -311,7 +316,6 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
"/assets/" + BUILD_REF + "/lib/path.js", "/assets/" + BUILD_REF + "/lib/path.js",
"/assets/" + BUILD_REF + "/lib/random.js", "/assets/" + BUILD_REF + "/lib/random.js",
"/assets/" + BUILD_REF + "/lib/settings.js", "/assets/" + BUILD_REF + "/lib/settings.js",
"/assets/" + BUILD_REF + "/lib/ajax.js",
"/assets/" + BUILD_REF + "/lib/animate.js", "/assets/" + BUILD_REF + "/lib/animate.js",
"/assets/" + BUILD_REF + "/lib/assert.js", "/assets/" + BUILD_REF + "/lib/assert.js",
"/assets/" + BUILD_REF + "/lib/dom.js", "/assets/" + BUILD_REF + "/lib/dom.js",
@ -321,7 +325,6 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
"/assets/" + BUILD_REF + "/lib/error.js", "/assets/" + BUILD_REF + "/lib/error.js",
"/assets/" + BUILD_REF + "/locales/index.js", "/assets/" + BUILD_REF + "/locales/index.js",
"/assets/" + BUILD_REF + "/model/config.js", "/assets/" + BUILD_REF + "/model/config.js",
"/assets/" + BUILD_REF + "/model/chromecast.js", "/assets/" + BUILD_REF + "/model/chromecast.js",
"/assets/" + BUILD_REF + "/model/session.js", "/assets/" + BUILD_REF + "/model/session.js",
@ -329,8 +332,9 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
"/assets/" + BUILD_REF + "/pages/ctrl_logout.js", "/assets/" + BUILD_REF + "/pages/ctrl_logout.js",
"/assets/" + BUILD_REF + "/pages/ctrl_error.js", "/assets/" + BUILD_REF + "/pages/ctrl_error.js",
},
{
"/assets/" + BUILD_REF + "/pages/ctrl_homepage.js", "/assets/" + BUILD_REF + "/pages/ctrl_homepage.js",
"/assets/" + BUILD_REF + "/pages/ctrl_connectpage.js", "/assets/" + BUILD_REF + "/pages/ctrl_connectpage.js",
"/assets/" + BUILD_REF + "/pages/ctrl_connectpage.css", "/assets/" + BUILD_REF + "/pages/ctrl_connectpage.css",
"/assets/" + BUILD_REF + "/pages/connectpage/ctrl_form.css", "/assets/" + BUILD_REF + "/pages/connectpage/ctrl_form.css",
@ -341,18 +345,24 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
"/assets/" + BUILD_REF + "/pages/connectpage/model_config.js", "/assets/" + BUILD_REF + "/pages/connectpage/model_config.js",
"/assets/" + BUILD_REF + "/pages/connectpage/ctrl_form_state.js", "/assets/" + BUILD_REF + "/pages/connectpage/ctrl_form_state.js",
"/assets/" + BUILD_REF + "/pages/ctrl_filespage.js",
"/assets/" + BUILD_REF + "/pages/ctrl_filespage.css",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_submenu.css",
"/assets/" + BUILD_REF + "/pages/filespage/model_acl.js",
"/assets/" + BUILD_REF + "/pages/filespage/cache.js",
"/assets/" + BUILD_REF + "/pages/filespage/thing.js", "/assets/" + BUILD_REF + "/pages/filespage/thing.js",
"/assets/" + BUILD_REF + "/pages/filespage/thing.css", "/assets/" + BUILD_REF + "/pages/filespage/thing.css",
},
{
"/assets/" + BUILD_REF + "/pages/ctrl_filespage.js",
"/assets/" + BUILD_REF + "/pages/ctrl_filespage.css",
"/assets/" + BUILD_REF + "/pages/filespage/model_acl.js",
"/assets/" + BUILD_REF + "/pages/filespage/cache.js",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_filesystem.js", "/assets/" + BUILD_REF + "/pages/filespage/ctrl_filesystem.js",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_filesystem.css", "/assets/" + BUILD_REF + "/pages/filespage/ctrl_filesystem.css",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_upload.js", "/assets/" + BUILD_REF + "/pages/filespage/ctrl_upload.js",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_upload.css", "/assets/" + BUILD_REF + "/pages/filespage/ctrl_upload.css",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_newitem.js",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_newitem.css",
},
{
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_submenu.js", "/assets/" + BUILD_REF + "/pages/filespage/ctrl_submenu.js",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_submenu.css",
"/assets/" + BUILD_REF + "/pages/filespage/state_config.js", "/assets/" + BUILD_REF + "/pages/filespage/state_config.js",
"/assets/" + BUILD_REF + "/pages/filespage/helper.js", "/assets/" + BUILD_REF + "/pages/filespage/helper.js",
"/assets/" + BUILD_REF + "/pages/filespage/model_files.js", "/assets/" + BUILD_REF + "/pages/filespage/model_files.js",
@ -367,8 +377,6 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
"/assets/" + BUILD_REF + "/pages/filespage/modal_delete.js", "/assets/" + BUILD_REF + "/pages/filespage/modal_delete.js",
"/assets/" + BUILD_REF + "/pages/filespage/state_selection.js", "/assets/" + BUILD_REF + "/pages/filespage/state_selection.js",
"/assets/" + BUILD_REF + "/pages/filespage/state_newthing.js", "/assets/" + BUILD_REF + "/pages/filespage/state_newthing.js",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_newitem.js",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_newitem.css",
// "/assets/" + BUILD_REF + "/pages/ctrl_viewerpage.js", // TODO: dynamic imports // "/assets/" + BUILD_REF + "/pages/ctrl_viewerpage.js", // TODO: dynamic imports
"/assets/" + BUILD_REF + "/pages/ctrl_viewerpage.css", "/assets/" + BUILD_REF + "/pages/ctrl_viewerpage.css",
@ -379,13 +387,20 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
"/assets/" + BUILD_REF + "/pages/viewerpage/application_downloader.css", "/assets/" + BUILD_REF + "/pages/viewerpage/application_downloader.css",
"/assets/" + BUILD_REF + "/pages/viewerpage/component_menubar.js", "/assets/" + BUILD_REF + "/pages/viewerpage/component_menubar.js",
"/assets/" + BUILD_REF + "/pages/viewerpage/component_menubar.css", "/assets/" + BUILD_REF + "/pages/viewerpage/component_menubar.css",
},
} }
var isDebug = os.Getenv("DEBUG") == "true" func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
isDebug := os.Getenv("DEBUG") == "true"
build := func(quality int) (bundlePlain []byte, bundleBr []byte, etag string) { buildChunks := func(quality int) (chunks [][]byte, chunksBr [][]byte, etags []string) {
var buf bytes.Buffer numChunks := len(preload)
for _, path := range paths { chunks = make([][]byte, numChunks+1)
chunksBr = make([][]byte, numChunks+1)
etags = make([]string, numChunks+1)
var fullBuf bytes.Buffer
for i := 0; i < numChunks; i++ {
var chunkBuf bytes.Buffer
for _, path := range preload[i] {
curPath := "/assets/" + strings.TrimPrefix(path, "/assets/"+BUILD_REF+"/") curPath := "/assets/" + strings.TrimPrefix(path, "/assets/"+BUILD_REF+"/")
f := applyPatch(curPath) f := applyPatch(curPath)
if f == nil { if f == nil {
@ -406,39 +421,51 @@ func ServeBundle() func(*App, http.ResponseWriter, *http.Request) {
Log.Warning("static::bundle msg=marshal_failed path=%s err=%s", path, err.Error()) Log.Warning("static::bundle msg=marshal_failed path=%s err=%s", path, err.Error())
continue continue
} }
fmt.Fprintf(&buf, "bundler.register(%q, %s);\n", WithBase(path), code) line := fmt.Sprintf("bundler.register(%q, %s);\n", WithBase(path), code)
chunkBuf.WriteString(line)
fullBuf.WriteString(line)
} }
etag = QuickHash(string(bundlePlain), 10) chunks[i+1] = chunkBuf.Bytes()
bundlePlain = buf.Bytes() chunksBr[i+1], _ = cbrotli.Encode(chunks[i+1], cbrotli.WriterOptions{Quality: quality})
if quality > 0 { etags[i+1] = QuickHash(string(chunks[i+1]), 10)
bundleBr, _ = cbrotli.Encode(bundlePlain, cbrotli.WriterOptions{Quality: quality})
} }
return bundlePlain, bundleBr, etag chunks[0] = fullBuf.Bytes()
chunksBr[0], _ = cbrotli.Encode(chunks[0], cbrotli.WriterOptions{Quality: quality})
etags[0] = QuickHash(string(chunks[0]), 10)
return chunks, chunksBr, etags
} }
quality := 11 quality := 11
if isDebug { if isDebug {
quality = 8 quality = 8
} }
bundlePlain, bundleBr, etag := build(quality) chunks, chunksBr, etags := buildChunks(quality)
return func(ctx *App, res http.ResponseWriter, req *http.Request) { return func(ctx *App, res http.ResponseWriter, req *http.Request) {
if isDebug { if isDebug {
bundlePlain, bundleBr, etag = build(quality) chunks, chunksBr, etags = buildChunks(quality)
}
chunkIndex := 0
if parsed, err := strconv.Atoi(req.URL.Query().Get("chunk")); err == nil {
chunkIndex = parsed
}
if chunkIndex >= len(chunks) {
http.NotFound(res, req)
return
} }
head := res.Header() head := res.Header()
head.Set("Content-Type", "application/javascript") head.Set("Content-Type", "application/javascript")
head.Set("Cache-Control", "no-cache") head.Set("Cache-Control", "no-cache")
head.Set("Etag", etag) head.Set("Etag", etags[chunkIndex])
if req.Header.Get("If-None-Match") == etag && etag != "" { if req.Header.Get("If-None-Match") == etags[chunkIndex] && etags[chunkIndex] != "" {
res.WriteHeader(http.StatusNotModified) res.WriteHeader(http.StatusNotModified)
return return
} else if strings.Contains(req.Header.Get("Accept-Encoding"), "br") && len(bundleBr) > 0 { } else if strings.Contains(req.Header.Get("Accept-Encoding"), "br") && len(chunksBr[chunkIndex]) > 0 {
head.Set("Content-Encoding", "br") head.Set("Content-Encoding", "br")
res.Write(bundleBr) res.Write(chunksBr[chunkIndex])
return return
} }
res.Write(bundlePlain) res.Write(chunks[chunkIndex])
} }
} }