Skip to content

Commit d08482f

Browse files
feature (turboload): decrease load time via sw
1 parent fddc98b commit d08482f

File tree

8 files changed

+521
-307
lines changed

8 files changed

+521
-307
lines changed

public/assets/components/sidebar.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,9 @@ async function ctrlNavigationPane(render, { $sidebar, nRestart }) {
134134
const $active = qs($sidebar, `[data-path="${chunk.toString()}"] a`);
135135
$active.classList.add("active");
136136
if (checkVisible($active) === false) {
137-
$active.offsetTop < window.innerHeight ?
138-
$sidebar.firstChild.scrollTo({top: 0, behavior: "smooth"}) :
139-
$active.scrollIntoView({ behavior: "smooth" });
137+
$active.offsetTop < window.innerHeight
138+
? $sidebar.firstChild.scrollTo({ top: 0, behavior: "smooth" })
139+
: $active.scrollIntoView({ behavior: "smooth" });
140140
}
141141
} catch (err) {}
142142

public/assets/css/designsystem.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
@import url("./designsystem_alert.css");
1414

1515
:root {
16-
--bg-color: #f9f9fa; /*#fafafa;*/
16+
--bg-color: #f9f9fa;
1717
--color: #57595A;
1818
--emphasis: #466372;
1919
--primary: #9AD1ED;

public/assets/sw.js

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
const VERSION = "v1";
2+
const CACHENAME = "assets -" + VERSION;
3+
4+
/*
5+
* This Service Worker is an optional optimisation to load the app faster.
6+
* Whenever using raw es module without any build, we had a large number
7+
* of assets getting through the network. When we looked through the
8+
* developer console -> network, and look at the timing, 98% of the time
9+
* was spent "waiting for the server response".
10+
* HTTP2/3 should solve that issue but we don't control the proxy side of
11+
* things of how people install Filestash, hence the idea to bulk download
12+
* as much as we can through SSE, store it onto a cache and get our
13+
* service worker to inject the response.
14+
* This approach alone make the app a lot faster to load but relies on
15+
* the server being able to bundle our assets via SSE.
16+
*
17+
* TODO:
18+
* - wait until browser support DecompressionStream("brotli") natively
19+
* and use that. As of 2025, downloading a brotli decompress library
20+
* make the gain br / gz negative for our app
21+
* - wait until Firefox support SSE within service worker. As of 2025,
22+
* someone was implementing it in Firefox but it's not everywhere yet
23+
* Once that's done, we want to be 100% sure everything is working great
24+
*/
25+
26+
self.addEventListener("install", (event) => {
27+
if (!self.EventSource) throw new Error("turboload not supported on this platform");
28+
29+
event.waitUntil((async() => {
30+
await self.skipWaiting();
31+
})());
32+
});
33+
34+
self.addEventListener("activate", (event) => {
35+
event.waitUntil((async() => {
36+
for (const name of await caches.keys()) await caches.delete(name);
37+
await self.clients.claim();
38+
})());
39+
});
40+
41+
self.addEventListener("fetch", (event) => {
42+
if (!event.request.url.startsWith(location.origin + "/assets/")) return;
43+
44+
event.respondWith((async() => {
45+
const cachedResponse = await caches.match(event.request);
46+
if (cachedResponse) return cachedResponse;
47+
return fetch(event.request);
48+
})());
49+
});
50+
51+
self.addEventListener("message", (event) => {
52+
if (event.data.type === "preload") handlePreloadMessage(
53+
event.data.payload,
54+
() => event.source.postMessage({ type: "preload", status: "ok" }),
55+
(err) => event.source.postMessage({ type: "preload", status: "error", msg: err.message }),
56+
);
57+
});
58+
59+
const handlePreloadMessage = (() => {
60+
const cleanup = [];
61+
return async(chunks, resolve, reject) => {
62+
cleanup.forEach((fn) => fn());
63+
try {
64+
caches.delete(CACHENAME);
65+
const cache = await caches.open(CACHENAME);
66+
await Promise.all(chunks.map((urls) => {
67+
return preload({ urls, cache, cleanup });
68+
}));
69+
resolve();
70+
} catch (err) {
71+
reject(err);
72+
}
73+
};
74+
})();
75+
76+
async function preload({ urls, cache, cleanup }) {
77+
const evtsrc = new self.EventSource("/assets/bundle?" + urls.map((url) => `url=${url}`).join("&"));
78+
cleanup.push(() => evtsrc.close());
79+
80+
let i = 0;
81+
const messageHandler = (resolve, event) => {
82+
const url = event.lastEventId;
83+
let mime = "application/octet-stream";
84+
if (url.endsWith(".css")) mime = "text/css";
85+
else if (url.endsWith(".js")) mime = "application/javascript";
86+
87+
i += 1;
88+
cache.put(
89+
location.origin + event.lastEventId,
90+
new Response(
91+
new Blob([Uint8Array.from(atob(event.data), (c) => c.charCodeAt(0))])
92+
.stream()
93+
.pipeThrough(new DecompressionStream("gzip")),
94+
{ headers: { "Content-Type": mime } },
95+
),
96+
);
97+
if (i === urls.length) {
98+
evtsrc.close();
99+
resolve();
100+
}
101+
};
102+
const errorHandler = (reject, err) => {
103+
evtsrc.close();
104+
reject(err);
105+
};
106+
107+
await new Promise((resolve, reject) => {
108+
evtsrc.onmessage = async(event) => messageHandler(resolve, event);
109+
evtsrc.onerror = (err) => errorHandler(reject, err);
110+
});
111+
}

0 commit comments

Comments
 (0)