feature (speed): improve loading speed

This commit is contained in:
MickaelK
2025-08-15 13:42:44 +10:00
parent 3bf3c71d8c
commit 72cebf4d6d
4 changed files with 192 additions and 90 deletions

View File

@ -1,4 +1,4 @@
const CACHENAME = "assets"; let VERSION = null;
/* /*
* This Service Worker is an optional optimisation to load the app faster. * This Service Worker is an optional optimisation to load the app faster.
@ -41,7 +41,7 @@ self.addEventListener("fetch", async(event) => {
if (!event.request.url.startsWith(location.origin + "/assets/")) return; if (!event.request.url.startsWith(location.origin + "/assets/")) return;
event.respondWith((async() => { event.respondWith((async() => {
const cache = await caches.open(CACHENAME); const cache = await caches.open(VERSION);
const cachedResponse = await cache.match(event.request); const cachedResponse = await cache.match(event.request);
if (cachedResponse) return cachedResponse; if (cachedResponse) return cachedResponse;
return fetch(event.request); return fetch(event.request);
@ -51,19 +51,42 @@ self.addEventListener("fetch", async(event) => {
self.addEventListener("message", (event) => { self.addEventListener("message", (event) => {
if (event.data.type === "preload") handlePreloadMessage( if (event.data.type === "preload") handlePreloadMessage(
event.data.payload, event.data.payload,
event.data.clear,
event.data.version,
() => event.source.postMessage({ type: "preload", status: "ok" }), () => event.source.postMessage({ type: "preload", status: "ok" }),
(err) => event.source.postMessage({ type: "preload", status: "error", msg: err.message }), (err) => event.source.postMessage({ type: "preload", status: "error", msg: err.message }),
); );
}); });
async function handlePreloadMessage(chunks, resolve, reject, id) { async function handlePreloadMessage(chunks, clear, version, resolve, reject) {
VERSION = version;
const cleanup = []; const cleanup = [];
try { try {
await caches.delete(CACHENAME); let execHTTP = true;
const cache = await caches.open(CACHENAME); await caches.keys().then(async(names) => {
await Promise.all(chunks.map((urls) => { for (let i=0; i<names.length; i++) {
return preload({ urls, cache, cleanup }); if (names[i] === VERSION && !clear) {
})); execHTTP = false;
return;
}
await caches.delete(names[i]);
}
});
if (execHTTP) {
const cache = await caches.open(VERSION);
chunks = await Promise.all(chunks.map(async(urls) => {
const missing = [];
await Promise.all(urls.map(async(url) => {
if (!await cache.match(location.origin + url)) missing.push(url);
}));
return missing;
}));
if (chunks.filter((urls) => urls.length > 0).length > 0) {
await Promise.all(chunks.map((urls) => {
return preload({ urls, cache, cleanup });
}));
}
}
resolve(); resolve();
} catch (err) { } catch (err) {
console.log("ERR", err); console.log("ERR", err);
@ -88,7 +111,7 @@ async function preload({ urls, cache, cleanup }) {
await cache.put( await cache.put(
location.origin + url, location.origin + url,
new Response( new Response(
decoder(new Blob([Uint8Array.from(atob(event.data), (c) => c.charCodeAt(0))]).stream()), decoder(new Blob([base128Decode(event.data)]).stream()),
{ headers: { "Content-Type": mime } }, { headers: { "Content-Type": mime } },
), ),
); );
@ -117,3 +140,22 @@ async function preload({ urls, cache, cleanup }) {
}; };
}); });
} }
function base128Decode(s) { // encoder is in server/ctrl/static.go -> encodeB128
const out = new Uint8Array(Math.floor((s.length * 7) / 8) + 1);
let acc = 0;
let bits = 0;
let oi = 0;
for (let i = 0; i < s.length; i++) {
const ch = s.charCodeAt(i);
const digit = ch & 0x7F; // undo 0x80 masking for NUL/LF/CR
acc = (acc << 7) | digit;
bits += 7;
while (bits >= 8) {
bits -= 8;
out[oi++] = (acc >> bits) & 0xFF;
acc &= (1 << bits) - 1;
}
}
return out.subarray(0, oi);
}

View File

@ -57,7 +57,12 @@
resolve(); resolve();
}); });
}); });
register.active.postMessage({ "type": "preload", "payload": URLS }); register.active.postMessage({
"type": "preload",
"payload": URLS,
"version": "{{ .version }}".substring(0, 7) + "::{{ .hash }}",
"clear": {{ .clear }},
});
await new Promise((resolve, reject) => navigator.serviceWorker.addEventListener("message", (event) => { await new Promise((resolve, reject) => navigator.serviceWorker.addEventListener("message", (event) => {
if (event.data && event.data.type === "preload") { if (event.data && event.data.type === "preload") {
if (event.data.status !== "ok") console.log(`turboload failure data=${JSON.stringify(event.data)}`); if (event.data.status !== "ok") console.log(`turboload failure data=${JSON.stringify(event.data)}`);

View File

@ -29,7 +29,7 @@ func PluginExportHandler(ctx *App, res http.ResponseWriter, req *http.Request) {
} }
} }
} }
SendSuccessResult(res, plgExports) SendSuccessResultWithEtagAndGzip(res, req, plgExports)
} }
func PluginStaticHandler(ctx *App, res http.ResponseWriter, req *http.Request) { func PluginStaticHandler(ctx *App, res http.ResponseWriter, req *http.Request) {

View File

@ -2,8 +2,8 @@ package ctrl
import ( import (
"bytes" "bytes"
"compress/gzip"
_ "embed" _ "embed"
"encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
@ -205,32 +205,42 @@ func ServeFile(chroot string) func(*App, http.ResponseWriter, *http.Request) {
} }
func ServeIndex(indexPath string) func(*App, http.ResponseWriter, *http.Request) { func ServeIndex(indexPath string) func(*App, http.ResponseWriter, *http.Request) {
// STEP1: pull the data from the embed
file, err := WWWPublic.Open(indexPath)
if err != nil {
return func(ctx *App, res http.ResponseWriter, req *http.Request) {
http.NotFound(res, req)
}
}
defer file.Close()
// STEP2: compile the template
b, err := io.ReadAll(file)
if err != nil {
return func(ctx *App, res http.ResponseWriter, req *http.Request) {
SendErrorResult(res, err)
}
}
tmpl := template.Must(template.New(indexPath).Parse(string(b)))
tmpl = template.Must(tmpl.Parse(string(TmplLoader)))
return func(ctx *App, res http.ResponseWriter, req *http.Request) { return func(ctx *App, res http.ResponseWriter, req *http.Request) {
head := res.Header() head := res.Header()
var out io.Writer = res
// STEP1: pull the data from the embed if strings.Contains(req.Header.Get("Accept-Encoding"), "gzip") {
file, err := WWWPublic.Open(indexPath) head.Set("Content-Encoding", "gzip")
if err != nil { gz := gzip.NewWriter(res)
http.NotFound(res, req) defer gz.Close()
return out = gz
}
defer file.Close()
// STEP2: compile the template
b, err := io.ReadAll(file)
if err != nil {
SendErrorResult(res, err)
return
} }
head.Set("Content-Type", "text/html") head.Set("Content-Type", "text/html")
res.WriteHeader(http.StatusOK) tmpl.Execute(out, map[string]any{
tmpl := template.Must(template.New(indexPath).Parse(string(b)))
tmpl = template.Must(tmpl.Parse(string(TmplLoader)))
tmpl.Execute(res, map[string]any{
"base": WithBase("/"), "base": WithBase("/"),
"version": BUILD_REF, "version": BUILD_REF,
"license": LICENSE, "license": LICENSE,
"preload": preload(), "preload": preload(),
"clear": req.Header.Get("Cache-Control") == "no-cache",
"hash": signature(),
}) })
} }
} }
@ -265,7 +275,7 @@ func ServeBundle(ctx *App, res http.ResponseWriter, req *http.Request) {
fmt.Fprintf(res, "id: %s\n", urls[i]) fmt.Fprintf(res, "id: %s\n", urls[i])
fmt.Fprintf(res, "data: ") fmt.Fprintf(res, "data: ")
b, _ := io.ReadAll(file) b, _ := io.ReadAll(file)
res.Write([]byte(base64.StdEncoding.EncodeToString(b))) res.Write([]byte(encodeB128(b)))
fmt.Fprintf(res, "\n\n") fmt.Fprintf(res, "\n\n")
res.(http.Flusher).Flush() res.(http.Flusher).Flush()
file.Close() file.Close()
@ -274,6 +284,34 @@ func ServeBundle(ctx *App, res http.ResponseWriter, req *http.Request) {
res.(http.Flusher).Flush() res.(http.Flusher).Flush()
} }
func encodeB128(src []byte) string { // decoder is in public/assets/sw.js
if len(src) == 0 {
return ""
}
out := make([]rune, 0, len(src)+len(src)/7) // N + N/7 runes (+tail)
var bits uint32
var n int // bits held in 'bits'
emit := func(d byte) {
if d == 0x00 || d == 0x0A || d == 0x0D { // NUL, LF, CR
out = append(out, rune(0x80|d)) // 2-byte UTF-8, still decodable as d&0x7F
return
}
out = append(out, rune(d)) // ASCII, 1-byte UTF-8
}
for _, b := range src {
bits = (bits << 8) | uint32(b)
n += 8
for n >= 7 {
n -= 7
emit(byte((bits >> n) & 0x7F))
}
}
if n > 0 { // tail
emit(byte((bits << (7 - n)) & 0x7F))
}
return string(out)
}
func applyPatch(filePath string) (file *bytes.Buffer) { func applyPatch(filePath string) (file *bytes.Buffer) {
var ( var (
outputBuffer bytes.Buffer outputBuffer bytes.Buffer
@ -329,10 +367,7 @@ func preload() string {
"/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",
},
{
"/assets/" + BUILD_REF + "/boot/ctrl_boot_frontoffice.js",
"/assets/" + BUILD_REF + "/locales/index.js",
"/assets/" + BUILD_REF + "/css/designsystem.css", "/assets/" + BUILD_REF + "/css/designsystem.css",
"/assets/" + BUILD_REF + "/css/designsystem_input.css", "/assets/" + BUILD_REF + "/css/designsystem_input.css",
"/assets/" + BUILD_REF + "/css/designsystem_textarea.css", "/assets/" + BUILD_REF + "/css/designsystem_textarea.css",
@ -348,13 +383,50 @@ func preload() string {
"/assets/" + BUILD_REF + "/css/designsystem_skeleton.css", "/assets/" + BUILD_REF + "/css/designsystem_skeleton.css",
"/assets/" + BUILD_REF + "/css/designsystem_utils.css", "/assets/" + BUILD_REF + "/css/designsystem_utils.css",
"/assets/" + BUILD_REF + "/css/designsystem_alert.css", "/assets/" + BUILD_REF + "/css/designsystem_alert.css",
"/assets/" + BUILD_REF + "/pages/filespage/modal.css",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_submenu.css",
"/assets/" + BUILD_REF + "/pages/filespage/modal_share.css",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_filesystem.css",
"/assets/" + BUILD_REF + "/pages/ctrl_filespage.css",
"/assets/" + BUILD_REF + "/pages/connectpage/ctrl_form.css",
"/assets/" + BUILD_REF + "/pages/viewerpage/component_menubar.css",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_upload.css",
"/assets/" + BUILD_REF + "/components/decorator_shell_filemanager.css",
},
{
"/assets/" + BUILD_REF + "/boot/ctrl_boot_frontoffice.js",
"/assets/" + BUILD_REF + "/boot/router_frontoffice.js",
"/assets/" + BUILD_REF + "/boot/common.js",
"/assets/" + BUILD_REF + "/components/loader.js", "/assets/" + BUILD_REF + "/components/loader.js",
"/assets/" + BUILD_REF + "/components/modal.js", "/assets/" + BUILD_REF + "/components/modal.js",
"/assets/" + BUILD_REF + "/components/modal.css", "/assets/" + BUILD_REF + "/components/modal.css",
"/assets/" + BUILD_REF + "/components/notification.js", "/assets/" + BUILD_REF + "/components/notification.js",
"/assets/" + BUILD_REF + "/components/notification.css", "/assets/" + BUILD_REF + "/components/notification.css",
"/assets/" + BUILD_REF + "/boot/router_frontoffice.js", "/assets/" + BUILD_REF + "/components/sidebar.js",
"/assets/" + BUILD_REF + "/components/sidebar.css",
"/assets/" + BUILD_REF + "/components/dropdown.js",
"/assets/" + BUILD_REF + "/components/decorator_shell_filemanager.js",
"/assets/" + BUILD_REF + "/helpers/loader.js", "/assets/" + BUILD_REF + "/helpers/loader.js",
"/assets/" + BUILD_REF + "/helpers/log.js",
"/assets/" + BUILD_REF + "/helpers/sdk.js",
"/assets/" + BUILD_REF + "/locales/index.js",
"/assets/" + BUILD_REF + "/lib/store.js",
"/assets/" + BUILD_REF + "/lib/form.js",
"/assets/" + BUILD_REF + "/lib/path.js",
"/assets/" + BUILD_REF + "/lib/random.js",
"/assets/" + BUILD_REF + "/model/config.js",
"/assets/" + BUILD_REF + "/model/chromecast.js",
"/assets/" + BUILD_REF + "/model/session.js",
"/assets/" + BUILD_REF + "/pages/filespage/model_acl.js",
"/assets/" + BUILD_REF + "/pages/filespage/cache.js",
"/assets/" + BUILD_REF + "/pages/filespage/thing.css",
"/assets/" + BUILD_REF + "/pages/viewerpage/component_menubar.js",
},
{
"/assets/" + BUILD_REF + "/components/form.js",
"/assets/" + BUILD_REF + "/components/icon.js",
"/assets/" + BUILD_REF + "/lib/settings.js",
"/assets/" + BUILD_REF + "/lib/skeleton/index.js", "/assets/" + BUILD_REF + "/lib/skeleton/index.js",
"/assets/" + BUILD_REF + "/lib/rx.js", "/assets/" + BUILD_REF + "/lib/rx.js",
"/assets/" + BUILD_REF + "/lib/ajax.js", "/assets/" + BUILD_REF + "/lib/ajax.js",
@ -364,80 +436,63 @@ func preload() string {
"/assets/" + BUILD_REF + "/lib/skeleton/router.js", "/assets/" + BUILD_REF + "/lib/skeleton/router.js",
"/assets/" + BUILD_REF + "/lib/skeleton/lifecycle.js", "/assets/" + BUILD_REF + "/lib/skeleton/lifecycle.js",
"/assets/" + BUILD_REF + "/lib/error.js", "/assets/" + BUILD_REF + "/lib/error.js",
"/assets/" + BUILD_REF + "/model/config.js",
"/assets/" + BUILD_REF + "/model/plugin.js",
"/assets/" + BUILD_REF + "/model/chromecast.js",
"/assets/" + BUILD_REF + "/model/session.js",
"/assets/" + BUILD_REF + "/helpers/log.js",
"/assets/" + BUILD_REF + "/boot/common.js",
"/assets/" + BUILD_REF + "/helpers/sdk.js",
"/assets/" + BUILD_REF + "/components/breadcrumb.js",
"/assets/" + BUILD_REF + "/components/breadcrumb.css",
"/assets/" + BUILD_REF + "/components/form.js",
"/assets/" + BUILD_REF + "/components/sidebar.js",
"/assets/" + BUILD_REF + "/components/sidebar.css",
"/assets/" + BUILD_REF + "/components/dropdown.js",
"/assets/" + BUILD_REF + "/components/icon.js",
"/assets/" + BUILD_REF + "/lib/store.js",
"/assets/" + BUILD_REF + "/lib/random.js",
"/assets/" + BUILD_REF + "/lib/form.js",
"/assets/" + BUILD_REF + "/lib/path.js",
"/assets/" + BUILD_REF + "/components/decorator_shell_filemanager.js",
"/assets/" + BUILD_REF + "/components/decorator_shell_filemanager.css",
"/assets/" + BUILD_REF + "/pages/ctrl_error.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_error.js",
"/assets/" + BUILD_REF + "/pages/ctrl_filespage.js",
"/assets/" + BUILD_REF + "/pages/ctrl_viewerpage.js",
"/assets/" + BUILD_REF + "/pages/ctrl_viewerpage.css",
"/assets/" + BUILD_REF + "/pages/ctrl_logout.js",
"/assets/" + BUILD_REF + "/pages/connectpage/ctrl_form.js", "/assets/" + BUILD_REF + "/pages/connectpage/ctrl_form.js",
"/assets/" + BUILD_REF + "/pages/connectpage/ctrl_forkme.js", "/assets/" + BUILD_REF + "/pages/connectpage/ctrl_forkme.js",
"/assets/" + BUILD_REF + "/pages/connectpage/ctrl_poweredby.js", "/assets/" + BUILD_REF + "/pages/connectpage/ctrl_poweredby.js",
"/assets/" + BUILD_REF + "/lib/path.js",
"/assets/" + BUILD_REF + "/lib/form.js",
"/assets/" + BUILD_REF + "/lib/settings.js",
"/assets/" + BUILD_REF + "/components/form.js",
"/assets/" + BUILD_REF + "/model/session.js",
"/assets/" + BUILD_REF + "/pages/ctrl_error.js",
"/assets/" + BUILD_REF + "/pages/connectpage/model_backend.js", "/assets/" + BUILD_REF + "/pages/connectpage/model_backend.js",
"/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 + "/lib/random.js", "/assets/" + BUILD_REF + "/pages/filespage/ctrl_filesystem.js",
"/assets/" + BUILD_REF + "/components/icon.js",
"/assets/" + BUILD_REF + "/pages/ctrl_connectpage.css",
"/assets/" + BUILD_REF + "/pages/connectpage/ctrl_form.css",
}, },
{ {
"/assets/" + BUILD_REF + "/pages/ctrl_filespage.js", "/assets/" + BUILD_REF + "/components/breadcrumb.js",
"/assets/" + BUILD_REF + "/pages/ctrl_filespage.css", "/assets/" + BUILD_REF + "/components/breadcrumb.css",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_filesystem.js", "/assets/" + BUILD_REF + "/components/skeleton.js",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_submenu.js",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_newitem.js",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_upload.js", "/assets/" + BUILD_REF + "/pages/filespage/ctrl_upload.js",
"/assets/" + BUILD_REF + "/pages/filespage/cache.js",
"/assets/" + BUILD_REF + "/pages/filespage/state_config.js", "/assets/" + BUILD_REF + "/pages/filespage/state_config.js",
"/assets/" + BUILD_REF + "/pages/filespage/thing.js",
"/assets/" + BUILD_REF + "/pages/filespage/state_newthing.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",
"/assets/" + BUILD_REF + "/pages/filespage/model_virtual_layer.js", "/assets/" + BUILD_REF + "/pages/filespage/model_virtual_layer.js",
"/assets/" + BUILD_REF + "/pages/filespage/modal_share.js",
"/assets/" + BUILD_REF + "/pages/filespage/modal_tag.js", "/assets/" + BUILD_REF + "/pages/filespage/modal_tag.js",
"/assets/" + BUILD_REF + "/pages/filespage/modal_tag.css",
"/assets/" + BUILD_REF + "/pages/filespage/modal_rename.js", "/assets/" + BUILD_REF + "/pages/filespage/modal_rename.js",
"/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/model_acl.js", "/assets/" + BUILD_REF + "/pages/filespage/state_newthing.js",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_newitem.js",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_filesystem.css",
"/assets/" + BUILD_REF + "/pages/filespage/thing.css",
"/assets/" + BUILD_REF + "/pages/filespage/modal.css",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_submenu.css",
"/assets/" + BUILD_REF + "/pages/filespage/modal_share.css",
"/assets/" + BUILD_REF + "/pages/filespage/modal_tag.css",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_newitem.css", "/assets/" + BUILD_REF + "/pages/filespage/ctrl_newitem.css",
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_upload.css", "/assets/" + BUILD_REF + "/pages/viewerpage/mimetype.js",
"/assets/" + BUILD_REF + "/pages/viewerpage/model_files.js",
"/assets/" + BUILD_REF + "/pages/viewerpage/common.js",
},
{
"/assets/" + BUILD_REF + "/pages/filespage/ctrl_submenu.js",
"/assets/" + BUILD_REF + "/pages/filespage/thing.js",
"/assets/" + BUILD_REF + "/pages/filespage/modal_share.js",
"/assets/" + BUILD_REF + "/pages/viewerpage/application_downloader.js",
"/assets/" + BUILD_REF + "/model/plugin.js",
}, },
}) })
return string(out) return string(out)
} }
func signature() string {
text := BUILD_REF
patches := Hooks.Get.StaticPatch()
for i := 0; i < len(patches); i++ {
text += string(patches[i])
}
entries, _ := os.ReadDir(GetAbsolutePath(PLUGIN_PATH))
for _, e := range entries {
stat, _ := e.Info()
text += fmt.Sprintf("[%s][%d][%s]", stat.Name(), stat.Size(), stat.ModTime().String())
}
return strings.ToLower(QuickHash(text, 3))
}