-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Discussed in #182
Originally posted by GrandSchtroumpf October 14, 2024
What is it about?
Use to prefetch modules
What's the motivation for this proposal?
Problems you are trying to solve:
We cannot use the Cache API to prefetch module in devmode which leads to poor performance
Goals you are trying to achieve:
Separate the speculative prefetch process from the caching process.
Any other context or information you want to share:
modulepreload
has been discussed and discarded in the early day of qwik because Firefox didn't support it. This is not the case anymore.- There is an existing
prefetchStrategy
->linkRel: 'modulepreload'
, but it only prefetch initial modules, those known at build time. - This topic has already been discussed in the late August 2024 and tests have been performed (https://github.com/thejackshelton/sw-playground)
- One of the main concern was that
modulepreload
could increase FCP/LCP. It was decided to change Qwik doc prefetch strategy to accumulate real user data to see if this is the case.
If modulepreload
affect negatively FCP/LCP it's still possible to use prefetchStrategy
-> linkInsert: 'js-append'
instead of 'html-append'
to delay to initial prefetch.
Proposed Solution / Feature
What do you propose?
Separate Speculative Prefetch from Caching
Speculative Prefetch (should work in dev mode) :
Use <link rel="modulepreload" />
to prefetch the modules:
- At build time, the server will gather all required modules and include
modulepreload
in the head (similar tolinkInsert: 'html-append'
prefetchStrategy) - on
DOMContentLoaded
to start listening onqprefetch
event - on
qprefetch
event, append new<link rel="modulepreload" />
tags with the bundle href. If modulepreload is not support just do a regularfetch
for the Service Worker to hook it.
Caching (should work only in prod mode) :
- on
DOMContentLoaded
register service worker, send all existingmodulepreload
hrefs to the service worker to cache. - on
fetch
cache the response if it's a qwik module
Code examples
This is a working code snippet. To use it
- in
entry.ssr
setprefetchStrategy
:
prefetchStrategy: {
implementation: {
linkInsert: 'html-append',
linkRel: 'modulepreload',
}
},
- In
root.tsx
remove<PrefetchServiceWorker />
&<PrefetchGraph />
and adduseModulePrelaod()
(see code snippet) - Create
sw.js
in thepublic
folder (see code snippet)
useModulePreload.tsx
import { sync$, useOnDocument, useOnWindow } from "@builder.io/qwik";
export const useModulePreload = () => {
// Initialize SW & cache
useOnWindow(
"DOMContentLoaded",
sync$(async () => {
const isDev = document.documentElement.getAttribute('q:render') === "ssr-dev";
if ("serviceWorker" in navigator && !isDev) {
await navigator.serviceWorker.register("/sw.js");
await navigator.serviceWorker.ready;
const modules = document.querySelectorAll<HTMLLinkElement>('link[rel="modulepreload"]');
const controller = navigator.serviceWorker.controller;
const hrefs = Array.from(modules).map((link) => link.href);
controller?.postMessage({ type: "init", value: hrefs });
}
})
);
// Listen on prefetch event
useOnDocument('qprefetch', sync$((event: CustomEvent<{ bundles: string[] }>) => {
const { bundles } = (event as CustomEvent).detail;
if (!Array.isArray(bundles)) return;
const base = document.documentElement.getAttribute("q:base") ?? "/";
const isDev = document.documentElement.getAttribute('q:render') === "ssr-dev";
const getHref = (bundle: string) => {
if (isDev) return bundle;
return `${base}${bundle}`.replace(/\/\./g, "");
}
const supportsModulePreload = document.querySelector('link')?.relList.supports('modulepreload');
for (const bundle of bundles) {
if (supportsModulePreload) {
const link = document.createElement("link");
link.rel = 'modulepreload';
link.fetchPriority = 'low';
link.href = getHref(bundle);
document.head.appendChild(link);
} else {
// triggers the sw if modulepreload is not supported
fetch(getHref(bundle));
}
}
}));
};
sw.js
const main = async () => {
let cache;
const fetchResponse = async (req) => {
// Check cache
const cachedResponse = await caches.match(req);
if (cachedResponse) return cachedResponse;
// Cache and return reponse
return fetch(req).then((res) => {
if (req.url.includes("q-") && req.url.endsWith('.js')) {
cache.put(req, res.clone());
}
return res;
})
}
self.addEventListener("activate", async (event) => {
event.waitUntil(caches.open("QwikModulePreload"));
});
self.addEventListener("message", async (message) => {
if (message.data.type === "init") {
cache ||= await caches.open("QwikModulePreload");
new Set(message.data.value).forEach(url => {
// force-cache to use disk cache if modulepreload was already executed
return fetchResponse(new Request(url, { cache: 'force-cache' }));
});
}
});
self.addEventListener("fetch", async (event) => {
cache ||= await caches.open("QwikModulePreload");
event.respondWith(fetchResponse(event.request));
});
};
main();
addEventListener("install", () => self.skipWaiting());
addEventListener("activate", () => self.clients.claim());
Links / References
MDN: https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel/modulepreload
Experiment: https://github.com/thejackshelton/sw-playground
app using useModulePreload
: https://qwik-playground.vercel.app/ (github: https://github.com/GrandSchtroumpf/qwik-hueeye)
Metadata
Metadata
Assignees
Labels
Type
Projects
Status
Status