From 60cead3f13cb6150afd73c2695127f2f3b46e270 Mon Sep 17 00:00:00 2001 From: maiieul Date: Fri, 5 Sep 2025 11:57:58 +0200 Subject: [PATCH 1/9] feat: MPA fallback for when SPA Link navigation is too slow --- .../src/runtime/src/link-component.tsx | 3 ++ packages/qwik/src/core/preloader/queue.ts | 43 ++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/packages/qwik-city/src/runtime/src/link-component.tsx b/packages/qwik-city/src/runtime/src/link-component.tsx index 9ae80e3e20d..355b9fc64fa 100644 --- a/packages/qwik-city/src/runtime/src/link-component.tsx +++ b/packages/qwik-city/src/runtime/src/link-component.tsx @@ -68,6 +68,7 @@ export const Link = component$((props) => { const preventDefault = clientNavPath ? sync$((event: MouseEvent, target: HTMLAnchorElement) => { if (!(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) { + (window as any).__qwikPendingFallbackHref = target.href; event.preventDefault(); } }) @@ -80,10 +81,12 @@ export const Link = component$((props) => { if (elm.hasAttribute('q:nbs')) { // Allow bootstrapping into useNavigate. await nav(location.href, { type: 'popstate' }); + delete (window as any).__qwikPendingFallbackHref; } else if (elm.href) { elm.setAttribute('aria-pressed', 'true'); await nav(elm.href, { forceReload: reload, replaceState, scroll }); elm.removeAttribute('aria-pressed'); + delete (window as any).__qwikPendingFallbackHref; } } }) diff --git a/packages/qwik/src/core/preloader/queue.ts b/packages/qwik/src/core/preloader/queue.ts index f31e4eb83ba..5c9d01a3e56 100644 --- a/packages/qwik/src/core/preloader/queue.ts +++ b/packages/qwik/src/core/preloader/queue.ts @@ -15,6 +15,7 @@ export let shouldResetFactor: boolean; let queueDirty: boolean; let preloadCount = 0; const queue: BundleImport[] = []; +const MPA_FALLBACK_THRESHOLD = 100; export const log = (...args: any[]) => { // eslint-disable-next-line no-console @@ -203,7 +204,23 @@ export const adjustProbabilities = ( * too. */ let newInverseProbability: number; - if (probability === 1 || (probability >= 0.99 && depsCount < 100)) { + + /** + * 100 deps to be preloaded at once would mean a ~10s delay on chrome 3G throttling. + * + * This can happen for Link components as they load all of the route bundles at once, but in + * this case we fallback to MPA. + * + * This should never happen for a normal component. But in case it happens, we set the limit + * based on MPA_FALLBACK_THRESHOLD + 1 === 101 (to ensure the fallback works), because if the + * user has to wait for 10s before anything happens it is possible that they try to click on + * something else, in which case we don't want to block reprioritization of this new event + * bundles for too long. (If browsers supported aborting modulepreloads, we wouldn't have to + * do this.) + * + * TODO: Set the limit to a number of kb instead of a number of bundles. + */ + if (probability === 1 || (probability >= 0.99 && depsCount <= MPA_FALLBACK_THRESHOLD + 1)) { depsCount++; // we're loaded at max probability, so elevate dynamic imports to 99% sure newInverseProbability = Math.min(0.01, 1 - dep.$importProbability$); @@ -217,6 +234,8 @@ export const adjustProbabilities = ( dep.$factor$ = factor; } + dispatchMPAFallback(); + adjustProbabilities(depBundle, newInverseProbability, seen); } } @@ -225,6 +244,7 @@ export const adjustProbabilities = ( export const handleBundle = (name: string, inverseProbability: number) => { const bundle = getBundle(name); if (bundle && bundle.$inverseProbability$ > inverseProbability) { + // prioritize the event bundles first adjustProbabilities(bundle, inverseProbability); } }; @@ -267,3 +287,24 @@ if (isBrowser) { } }); } + +/** + * On chrome 3G throttling, 10kb takes ~1s to download Bundles weight ~1kb on average, so 100 + * bundles is ~100kb which takes ~10s to download. + * + * We want to fallback to MPA if more than 100 bundles are queued because MPA is always faster than + * ~10s (usually between 3-7s). + * + * Note: if the next route bundles have already been preloaded, the fallback won't be triggered. + * + * TODO: get total kb size and compare with 100kb instead of relying on the number of bundles. + */ +const dispatchMPAFallback = () => { + const nextRouteBundles = queue.filter((item) => item.$inverseProbability$ <= 0.1); + if (nextRouteBundles.length >= MPA_FALLBACK_THRESHOLD) { + const href = (window as any).__qwikPendingFallbackHref; + if (href) { + window.location.href = href; + } + } +}; From 53620072de301d1e817503aaab88447f14f8f7cf Mon Sep 17 00:00:00 2001 From: maiieul Date: Fri, 5 Sep 2025 11:58:03 +0200 Subject: [PATCH 2/9] fix: preloader-test layout.tsx Link hrefs --- starters/apps/preloader-test/src/routes/layout.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/starters/apps/preloader-test/src/routes/layout.tsx b/starters/apps/preloader-test/src/routes/layout.tsx index bf44e47e9d8..cf8c3eae82b 100644 --- a/starters/apps/preloader-test/src/routes/layout.tsx +++ b/starters/apps/preloader-test/src/routes/layout.tsx @@ -65,10 +65,10 @@ export default component$(() => {