Skip to content

Commit

Permalink
feature (turboload): decrease load time via sw
Browse files Browse the repository at this point in the history
  • Loading branch information
mickael-kerjean committed Jan 27, 2025
1 parent fddc98b commit c3f2c57
Show file tree
Hide file tree
Showing 8 changed files with 521 additions and 307 deletions.
6 changes: 3 additions & 3 deletions public/assets/components/sidebar.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,9 @@ async function ctrlNavigationPane(render, { $sidebar, nRestart }) {
const $active = qs($sidebar, `[data-path="${chunk.toString()}"] a`);
$active.classList.add("active");
if (checkVisible($active) === false) {
$active.offsetTop < window.innerHeight ?
$sidebar.firstChild.scrollTo({top: 0, behavior: "smooth"}) :
$active.scrollIntoView({ behavior: "smooth" });
$active.offsetTop < window.innerHeight
? $sidebar.firstChild.scrollTo({ top: 0, behavior: "smooth" })
: $active.scrollIntoView({ behavior: "smooth" });
}
} catch (err) {}

Expand Down
2 changes: 1 addition & 1 deletion public/assets/css/designsystem.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
@import url("./designsystem_alert.css");

:root {
--bg-color: #f9f9fa; /*#fafafa;*/
--bg-color: #f9f9fa;
--color: #57595A;
--emphasis: #466372;
--primary: #9AD1ED;
Expand Down
111 changes: 111 additions & 0 deletions public/assets/sw.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
const VERSION = "v1";
const CACHENAME = "assets";

/*
* This Service Worker is an optional optimisation to load the app faster.
* Whenever using raw es module without any build, we had a large number
* of assets getting through the network. When we looked through the
* developer console -> network, and look at the timing, 98% of the time
* was spent "waiting for the server response".
* HTTP2/3 should solve that issue but we don't control the proxy side of
* things of how people install Filestash, hence the idea to bulk download
* as much as we can through SSE, store it onto a cache and get our
* service worker to inject the response.
* This approach alone make the app a lot faster to load but relies on
* the server being able to bundle our assets via SSE.
*
* TODO:
* - wait until browser support DecompressionStream("brotli") natively
* and use that. As of 2025, downloading a brotli decompress library
* make the gain br / gz negative for our app
* - wait until Firefox support SSE within service worker. As of 2025,
* someone was implementing it in Firefox but it's not everywhere yet
* Once that's done, we want to be 100% sure everything is working great
*/

self.addEventListener("install", (event) => {
if (!self.EventSource) throw new Error("turboload not supported on this platform");

event.waitUntil((async() => {
await self.skipWaiting();
})());
});

self.addEventListener("activate", (event) => {
event.waitUntil((async() => {
for (const name of await caches.keys()) await caches.delete(name);
await self.clients.claim();
})());
});

self.addEventListener("fetch", (event) => {
if (!event.request.url.startsWith(location.origin + "/assets/")) return;

event.respondWith((async() => {
const cachedResponse = await caches.match(event.request);
if (cachedResponse) return cachedResponse;
return fetch(event.request);
})());
});

self.addEventListener("message", (event) => {
if (event.data.type === "preload") handlePreloadMessage(
event.data.payload,
() => event.source.postMessage({ type: "preload", status: "ok" }),
(err) => event.source.postMessage({ type: "preload", status: "error", msg: err.message }),
);
});

const handlePreloadMessage = (() => {
const cleanup = [];
return async(chunks, resolve, reject) => {
cleanup.forEach((fn) => fn());
try {
caches.delete(CACHENAME);
const cache = await caches.open(CACHENAME);
await Promise.all(chunks.map((urls) => {
return preload({ urls, cache, cleanup });
}));
resolve();
} catch (err) {
reject(err);
}
};
})();

async function preload({ urls, cache, cleanup }) {
const evtsrc = new self.EventSource("/assets/bundle?" + urls.map((url) => `url=${url}`).join("&"));
cleanup.push(() => evtsrc.close());

let i = 0;
const messageHandler = (resolve, event) => {
const url = event.lastEventId;
let mime = "application/octet-stream";
if (url.endsWith(".css")) mime = "text/css";
else if (url.endsWith(".js")) mime = "application/javascript";

i += 1;
cache.put(
location.origin + event.lastEventId,
new Response(
new Blob([Uint8Array.from(atob(event.data), (c) => c.charCodeAt(0))])
.stream()
.pipeThrough(new DecompressionStream("gzip")),
{ headers: { "Content-Type": mime } },
),
);
if (i === urls.length) {
evtsrc.close();
resolve();
}
};
const errorHandler = (reject, err) => {
evtsrc.close();
reject(err);
};

await new Promise((resolve, reject) => {
evtsrc.onmessage = async(event) => messageHandler(resolve, event);
evtsrc.onerror = (err) => errorHandler(reject, err);
});
}
Loading

0 comments on commit c3f2c57

Please sign in to comment.