\n\n_(Optional)_ \\*\\*Defaults to \\_true\\_.\\*\\*\n\nWhether Qwik should prefetch and cache the target page of this \\*\\*`Link`\\*\\*, this includes invoking any \\*\\*`routeLoader$`\\*\\*, \\*\\*`onGet`\\*\\*, etc.\n\nThis \\*\\*improves UX performance\\*\\* for client-side (\\*\\*SPA\\*\\*) navigations.\n\nPrefetching occurs when a the Link enters the viewport in production (\\*\\*`on:qvisible`\\*\\*), or with \\*\\*`mouseover`/`focus`\\*\\* during dev.\n\nPrefetching will not occur if the user has the \\*\\*data saver\\*\\* setting enabled.\n\nSetting this value to \\*\\*`\"js\"`\\*\\* will prefetch only javascript bundles required to render this page on the client, \\*\\*`false`\\*\\* will disable prefetching altogether.\n\n\n
\n\n_(Optional)_ \\*\\*Defaults to \\_true\\_.\\*\\*\n\nWhether Qwik should fallback to MPA navigation if too many bundles are queued for preloading.\n\n\n
\n
\n\n[prefetch?](#)\n\n\n
\n\n\n
\n\nboolean \\| 'js'\n\n\n
\n\n_(Optional)_ \\*\\*Defaults to \\_true\\_.\\*\\*\n\nWhether Qwik should prefetch and cache the target page of this \\*\\*`Link`\\*\\*, this includes invoking any \\*\\*`routeLoader$`\\*\\*, \\*\\*`onGet`\\*\\*, etc.\n\nThis \\*\\*improves UX performance\\*\\* for client-side (\\*\\*SPA\\*\\*) navigations.\n\nPrefetching occurs when a the Link enters the viewport in production (\\*\\*`on:qvisible`\\*\\*), or with \\*\\*`mouseover`/`focus`\\*\\* during dev.\n\nPrefetching will not occur if the user has the \\*\\*data saver\\*\\* setting enabled.\n\nSetting this value to \\*\\*`\"js\"`\\*\\* will prefetch only javascript bundles required to render this page on the client, \\*\\*`false`\\*\\* will disable prefetching altogether.\n\n\n
\n\n**Returns:**\n\nstring",
"mdFile": "qwik.path.dirname.md"
},
+ {
+ "name": "enableFallbackToMpa",
+ "id": "experimentalfeatures-enablefallbacktompa",
+ "hierarchy": [
+ {
+ "name": "ExperimentalFeatures",
+ "id": "experimentalfeatures-enablefallbacktompa"
+ },
+ {
+ "name": "enableFallbackToMpa",
+ "id": "experimentalfeatures-enablefallbacktompa"
+ }
+ ],
+ "kind": "EnumMember",
+ "content": "",
+ "mdFile": "qwik.experimentalfeatures.enablefallbacktompa.md"
+ },
{
"name": "enableRequestRewrite",
"id": "experimentalfeatures-enablerequestrewrite",
@@ -147,7 +164,7 @@
}
],
"kind": "Enum",
- "content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nUse `__EXPERIMENTAL__.x` to check if feature `x` is enabled. It will be replaced with `true` or `false` via an exact string replacement.\n\nAdd experimental features to this enum definition.\n\n\n```typescript\nexport declare enum ExperimentalFeatures \n```\n\n\n
\n\nMember\n\n\n
\n\nValue\n\n\n
\n\nDescription\n\n\n
\n
\n\nenableRequestRewrite\n\n\n
\n\n`\"enableRequestRewrite\"`\n\n\n
\n\n**_(ALPHA)_** Enable request.rewrite()\n\n\n
\n
\n\nnoSPA\n\n\n
\n\n`\"noSPA\"`\n\n\n
\n\n**_(ALPHA)_** Disable SPA navigation handler in Qwik City\n\n\n
\n
\n\npreventNavigate\n\n\n
\n\n`\"preventNavigate\"`\n\n\n
\n\n**_(ALPHA)_** Enable the usePreventNavigate hook\n\n\n
\n
\n\nvalibot\n\n\n
\n\n`\"valibot\"`\n\n\n
\n\n**_(ALPHA)_** Enable the Valibot form validation\n\n\n
\n
",
+ "content": "> This API is provided as an alpha preview for developers and may change based on feedback that we receive. Do not use this API in a production environment.\n> \n\nUse `__EXPERIMENTAL__.x` to check if feature `x` is enabled. It will be replaced with `true` or `false` via an exact string replacement.\n\nAdd experimental features to this enum definition.\n\n\n```typescript\nexport declare enum ExperimentalFeatures \n```\n\n\n
\n\nMember\n\n\n
\n\nValue\n\n\n
\n\nDescription\n\n\n
\n
\n\nenableFallbackToMpa\n\n\n
\n\n`\"enableFallbackToMpa\"`\n\n\n
\n\n**_(ALPHA)_** Enable falling back to MPA when SPA navigation is slow, which can happen with a big page during preloading\n\n\n
\n
\n\nenableRequestRewrite\n\n\n
\n\n`\"enableRequestRewrite\"`\n\n\n
\n\n**_(ALPHA)_** Enable request.rewrite()\n\n\n
\n
\n\nnoSPA\n\n\n
\n\n`\"noSPA\"`\n\n\n
\n\n**_(ALPHA)_** Disable SPA navigation handler in Qwik City\n\n\n
\n
\n\npreventNavigate\n\n\n
\n\n`\"preventNavigate\"`\n\n\n
\n\n**_(ALPHA)_** Enable the usePreventNavigate hook\n\n\n
\n
\n\nvalibot\n\n\n
\n\n`\"valibot\"`\n\n\n
\n\n**_(ALPHA)_** Enable the Valibot form validation\n\n\n
+
+**_(ALPHA)_** Enable falling back to MPA when SPA navigation is slow, which can happen with a big page during preloading
+
+
+
+
enableRequestRewrite
diff --git a/packages/qwik-city/src/runtime/src/contexts.ts b/packages/qwik-city/src/runtime/src/contexts.ts
index f0fc2f36080..05707c12741 100644
--- a/packages/qwik-city/src/runtime/src/contexts.ts
+++ b/packages/qwik-city/src/runtime/src/contexts.ts
@@ -29,3 +29,5 @@ export const RouteInternalContext =
export const RoutePreventNavigateContext =
/*#__PURE__*/ createContextId('qc-p');
+
+export const fallbackToMpaContext = /*#__PURE__*/ createContextId<{ default: boolean }>('qc-f');
diff --git a/packages/qwik-city/src/runtime/src/link-component.tsx b/packages/qwik-city/src/runtime/src/link-component.tsx
index 9ae80e3e20d..d9a5583ccd6 100644
--- a/packages/qwik-city/src/runtime/src/link-component.tsx
+++ b/packages/qwik-city/src/runtime/src/link-component.tsx
@@ -17,6 +17,7 @@ import { preloadRouteBundles } from './client-navigate';
import { isDev } from '@builder.io/qwik';
// @ts-expect-error we don't have types for the preloader yet
import { p as preload } from '@builder.io/qwik/preloader';
+// import { fallbackToMpaContext } from './contexts';
/** @public */
export const Link = component$((props) => {
@@ -24,14 +25,23 @@ export const Link = component$((props) => {
const loc = useLocation();
const originalHref = props.href;
const anchorRef = useSignal();
+
const {
onClick$,
prefetch: prefetchProp,
reload,
replaceState,
scroll,
+ fallbackToMpa: fallbackToMpaProp,
...linkProps
} = (() => props)();
+
+ // const defaultFallbackToMpa = useContext(fallbackToMpaContext).default;
+
+ // const fallbackToMpa = __EXPERIMENTAL__.enableFallbackToMpa
+ // ? untrack(() => Boolean(fallbackToMpaProp ?? defaultFallbackToMpa))
+ // : undefined;
+
const clientNavPath = untrack(() => getClientNavPath({ ...linkProps, reload }, loc));
linkProps.href = clientNavPath || originalHref;
@@ -66,7 +76,7 @@ export const Link = component$((props) => {
: undefined;
const preventDefault = clientNavPath
- ? sync$((event: MouseEvent, target: HTMLAnchorElement) => {
+ ? sync$((event: MouseEvent) => {
if (!(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) {
event.preventDefault();
}
@@ -89,9 +99,27 @@ export const Link = component$((props) => {
})
: undefined;
- const handlePreload = $((_: any, elm: HTMLAnchorElement) => {
- const url = new URL(elm.href);
+ const handlePreload = $((_: any, target: HTMLAnchorElement) => {
+ if (!target?.href) {
+ return;
+ }
+ const onTooMany = (event: Event) => {
+ const userEventPreloads = (event as CustomEvent).detail;
+ /**
+ * On chrome 3G throttling, 10kb takes ~1s to download. Bundles weight ~1kb on average, so 100
+ * bundles is ~100kb which takes ~10s to download.
+ *
+ * This can serve to fallback to MPA when SPA navigation takes more than 10s. Or in extreme
+ * cases, if a component needs more than a 100 bundles, display a spinner.
+ */
+ if (userEventPreloads.count >= 100) {
+ location.assign(target.href);
+ }
+ };
+ window.addEventListener('userEventPreloads', onTooMany);
+ const url = new URL(target.href);
preloadRouteBundles(url.pathname, 1);
+ window.removeEventListener('userEventPreloads', onTooMany);
});
useVisibleTask$(({ track }) => {
@@ -166,4 +194,11 @@ export interface LinkProps extends AnchorAttributes {
reload?: boolean;
replaceState?: boolean;
scroll?: boolean;
+
+ /**
+ * **Defaults to _true_.**
+ *
+ * Whether Qwik should fallback to MPA navigation if too many bundles are queued for preloading.
+ */
+ fallbackToMpa?: boolean;
}
diff --git a/packages/qwik-city/src/runtime/src/qwik-city-component.tsx b/packages/qwik-city/src/runtime/src/qwik-city-component.tsx
index 038f73c6667..4d4c82533f2 100644
--- a/packages/qwik-city/src/runtime/src/qwik-city-component.tsx
+++ b/packages/qwik-city/src/runtime/src/qwik-city-component.tsx
@@ -24,6 +24,7 @@ import {
DocumentHeadContext,
RouteActionContext,
RouteInternalContext,
+ fallbackToMpaContext,
RouteLocationContext,
RouteNavigateContext,
RoutePreventNavigateContext,
@@ -89,6 +90,12 @@ export interface QwikCityProps {
* @see https://caniuse.com/mdn-api_viewtransition
*/
viewTransition?: boolean;
+
+ /**
+ * Whether Qwik should fallback to MPA navigation if too many bundles are queued for preloading
+ * during SPA navigation.
+ */
+ fallbackToMpa?: { default: boolean };
}
// Gets populated by registerPreventNav on the client
@@ -307,6 +314,7 @@ export const QwikCityProvider = component$((props) => {
useContextProvider(RouteActionContext, actionState);
useContextProvider(RouteInternalContext, routeInternal);
useContextProvider(RoutePreventNavigateContext, registerPreventNav);
+ useContextProvider(fallbackToMpaContext, props.fallbackToMpa ?? { default: false });
useTask$(({ track }) => {
async function run() {
@@ -646,6 +654,7 @@ export interface QwikCityMockProps {
url?: string;
params?: Record;
goto?: RouteNavigate;
+ fallbackToMpa?: { default: boolean };
}
/** @public */
@@ -693,6 +702,8 @@ export const QwikCityMockProvider = component$((props) => {
useContextProvider(RouteStateContext, loaderState);
useContextProvider(RouteActionContext, actionState);
useContextProvider(RouteInternalContext, routeInternal);
+ console.log('props.fallbackToMpa', props.fallbackToMpa?.default);
+ useContextProvider(fallbackToMpaContext, props.fallbackToMpa ?? { default: false });
return ;
});
diff --git a/packages/qwik-city/src/runtime/src/qwik-city.runtime.api.md b/packages/qwik-city/src/runtime/src/qwik-city.runtime.api.md
index e4773ede255..5422e48a205 100644
--- a/packages/qwik-city/src/runtime/src/qwik-city.runtime.api.md
+++ b/packages/qwik-city/src/runtime/src/qwik-city.runtime.api.md
@@ -277,6 +277,7 @@ export const Link: Component;
//
// @public (undocumented)
export interface LinkProps extends AnchorAttributes {
+ fallbackToMpa?: boolean;
prefetch?: boolean | 'js';
// (undocumented)
reload?: boolean;
@@ -330,6 +331,10 @@ export const QWIK_CITY_SCROLLER = "_qCityScroller";
// @public (undocumented)
export interface QwikCityMockProps {
+ // (undocumented)
+ fallbackToMpa?: {
+ default: boolean;
+ };
// (undocumented)
goto?: RouteNavigate;
// (undocumented)
@@ -359,6 +364,9 @@ export interface QwikCityPlan {
// @public (undocumented)
export interface QwikCityProps {
+ fallbackToMpa?: {
+ default: boolean;
+ };
viewTransition?: boolean;
}
diff --git a/packages/qwik/src/core/preloader/preloader.unit.ts b/packages/qwik/src/core/preloader/preloader.unit.ts
index 3cf4fa49f5a..b7568c3abc9 100644
--- a/packages/qwik/src/core/preloader/preloader.unit.ts
+++ b/packages/qwik/src/core/preloader/preloader.unit.ts
@@ -21,5 +21,5 @@ test('preloader script', () => {
* dereference objects etc, but that actually results in worse compression
*/
const compressed = compress(Buffer.from(preLoader), { mode: 1, quality: 11 });
- expect([compressed.length, preLoader.length]).toEqual([1818, 5417]);
+ expect([compressed.length, preLoader.length]).toEqual([2031, 6129]);
});
diff --git a/packages/qwik/src/core/preloader/queue.ts b/packages/qwik/src/core/preloader/queue.ts
index f31e4eb83ba..fafbe0e8455 100644
--- a/packages/qwik/src/core/preloader/queue.ts
+++ b/packages/qwik/src/core/preloader/queue.ts
@@ -74,6 +74,11 @@ export const trigger = () => {
}
sortQueue();
while (queue.length) {
+ const userEventPreloads = queue.filter((item) => item.$inverseProbability$ <= 0.1);
+ dispatchEvent(
+ new CustomEvent('userEventPreloads', { detail: { count: userEventPreloads.length } })
+ );
+
const bundle = queue[0];
const inverseProbability = bundle.$inverseProbability$;
const probability = 1 - inverseProbability;
@@ -203,7 +208,21 @@ 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 OVERLY_SLOW_REPRIORITIZED_PRELOADING_DEFAULT_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.)
+ */
+ if (probability === 1 || (probability >= 0.99 && depsCount <= 101)) {
depsCount++;
// we're loaded at max probability, so elevate dynamic imports to 99% sure
newInverseProbability = Math.min(0.01, 1 - dep.$importProbability$);
@@ -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);
}
};
diff --git a/packages/qwik/src/optimizer/src/plugins/plugin.ts b/packages/qwik/src/optimizer/src/plugins/plugin.ts
index 35ac8c78b3a..6e300334a57 100644
--- a/packages/qwik/src/optimizer/src/plugins/plugin.ts
+++ b/packages/qwik/src/optimizer/src/plugins/plugin.ts
@@ -66,6 +66,11 @@ const CLIENT_STRIP_CTX_NAME = [
* @alpha
*/
export enum ExperimentalFeatures {
+ /**
+ * Enable falling back to MPA when SPA navigation is slow, which can happen with a big page during
+ * preloading
+ */
+ enableFallbackToMpa = 'enableFallbackToMpa',
/** Enable the usePreventNavigate hook */
preventNavigate = 'preventNavigate',
/** Enable the Valibot form validation */
diff --git a/packages/qwik/src/optimizer/src/qwik.optimizer.api.md b/packages/qwik/src/optimizer/src/qwik.optimizer.api.md
index 6030200d353..75119e826bb 100644
--- a/packages/qwik/src/optimizer/src/qwik.optimizer.api.md
+++ b/packages/qwik/src/optimizer/src/qwik.optimizer.api.md
@@ -52,6 +52,7 @@ export type EntryStrategy = InlineEntryStrategy | HoistEntryStrategy | SingleEnt
// @alpha
export enum ExperimentalFeatures {
+ enableFallbackToMpa = "enableFallbackToMpa",
enableRequestRewrite = "enableRequestRewrite",
noSPA = "noSPA",
preventNavigate = "preventNavigate",
diff --git a/starters/apps/preloader-test/src/root.tsx b/starters/apps/preloader-test/src/root.tsx
index 1fdb6ca6e59..573c8a9ff29 100644
--- a/starters/apps/preloader-test/src/root.tsx
+++ b/starters/apps/preloader-test/src/root.tsx
@@ -13,7 +13,7 @@ export default component$(() => {
*/
return (
-
+
diff --git a/starters/apps/preloader-test/src/routes/fallback/index.tsx b/starters/apps/preloader-test/src/routes/fallback/index.tsx
new file mode 100644
index 00000000000..4d0874bea26
--- /dev/null
+++ b/starters/apps/preloader-test/src/routes/fallback/index.tsx
@@ -0,0 +1,224 @@
+import { component$ } from "@builder.io/qwik";
+import { Link } from "@builder.io/qwik-city";
+import Counter1 from "~/components/generated/counter1";
+import Counter2 from "~/components/generated/counter2";
+import Counter3 from "~/components/generated/counter3";
+import Counter4 from "~/components/generated/counter4";
+import Counter5 from "~/components/generated/counter5";
+import Counter6 from "~/components/generated/counter6";
+import Counter7 from "~/components/generated/counter7";
+import Counter8 from "~/components/generated/counter8";
+import Counter9 from "~/components/generated/counter9";
+import Counter10 from "~/components/generated/counter10";
+import Counter11 from "~/components/generated/counter11";
+import Counter12 from "~/components/generated/counter12";
+import Counter13 from "~/components/generated/counter13";
+import Counter14 from "~/components/generated/counter14";
+import Counter15 from "~/components/generated/counter15";
+import Counter16 from "~/components/generated/counter16";
+import Counter17 from "~/components/generated/counter17";
+import Counter18 from "~/components/generated/counter18";
+import Counter19 from "~/components/generated/counter19";
+import Counter20 from "~/components/generated/counter20";
+import Counter21 from "~/components/generated/counter21";
+import Counter22 from "~/components/generated/counter22";
+import Counter23 from "~/components/generated/counter23";
+import Counter24 from "~/components/generated/counter24";
+import Counter25 from "~/components/generated/counter25";
+import Counter26 from "~/components/generated/counter26";
+import Counter27 from "~/components/generated/counter27";
+import Counter28 from "~/components/generated/counter28";
+import Counter29 from "~/components/generated/counter29";
+import Counter30 from "~/components/generated/counter30";
+import Counter31 from "~/components/generated/counter31";
+import Counter32 from "~/components/generated/counter32";
+import Counter33 from "~/components/generated/counter33";
+import Counter34 from "~/components/generated/counter34";
+import Counter35 from "~/components/generated/counter35";
+import Counter36 from "~/components/generated/counter36";
+import Counter37 from "~/components/generated/counter37";
+import Counter38 from "~/components/generated/counter38";
+import Counter39 from "~/components/generated/counter39";
+import Counter40 from "~/components/generated/counter40";
+import Counter41 from "~/components/generated/counter41";
+import Counter42 from "~/components/generated/counter42";
+import Counter43 from "~/components/generated/counter43";
+import Counter44 from "~/components/generated/counter44";
+import Counter45 from "~/components/generated/counter45";
+import Counter46 from "~/components/generated/counter46";
+import Counter47 from "~/components/generated/counter47";
+import Counter48 from "~/components/generated/counter48";
+import Counter49 from "~/components/generated/counter49";
+import Counter50 from "~/components/generated/counter50";
+import Counter51 from "~/components/generated/counter51";
+import Counter52 from "~/components/generated/counter52";
+import Counter53 from "~/components/generated/counter53";
+import Counter54 from "~/components/generated/counter54";
+import Counter55 from "~/components/generated/counter55";
+import Counter56 from "~/components/generated/counter56";
+import Counter57 from "~/components/generated/counter57";
+import Counter58 from "~/components/generated/counter58";
+import Counter59 from "~/components/generated/counter59";
+import Counter60 from "~/components/generated/counter60";
+import Counter61 from "~/components/generated/counter61";
+import Counter62 from "~/components/generated/counter62";
+import Counter63 from "~/components/generated/counter63";
+import Counter64 from "~/components/generated/counter64";
+import Counter65 from "~/components/generated/counter65";
+import Counter66 from "~/components/generated/counter66";
+import Counter67 from "~/components/generated/counter67";
+import Counter68 from "~/components/generated/counter68";
+import Counter69 from "~/components/generated/counter69";
+import Counter70 from "~/components/generated/counter70";
+import Counter71 from "~/components/generated/counter71";
+import Counter72 from "~/components/generated/counter72";
+import Counter73 from "~/components/generated/counter73";
+import Counter74 from "~/components/generated/counter74";
+import Counter75 from "~/components/generated/counter75";
+import Counter76 from "~/components/generated/counter76";
+import Counter77 from "~/components/generated/counter77";
+import Counter78 from "~/components/generated/counter78";
+import Counter79 from "~/components/generated/counter79";
+import Counter80 from "~/components/generated/counter80";
+import Counter81 from "~/components/generated/counter81";
+import Counter82 from "~/components/generated/counter82";
+import Counter83 from "~/components/generated/counter83";
+import Counter84 from "~/components/generated/counter84";
+import Counter85 from "~/components/generated/counter85";
+import Counter86 from "~/components/generated/counter86";
+import Counter87 from "~/components/generated/counter87";
+import Counter88 from "~/components/generated/counter88";
+import Counter89 from "~/components/generated/counter89";
+import Counter90 from "~/components/generated/counter90";
+import Counter91 from "~/components/generated/counter91";
+import Counter92 from "~/components/generated/counter92";
+import Counter93 from "~/components/generated/counter93";
+import Counter94 from "~/components/generated/counter94";
+import Counter95 from "~/components/generated/counter95";
+import Counter96 from "~/components/generated/counter96";
+import Counter97 from "~/components/generated/counter97";
+import Counter98 from "~/components/generated/counter98";
+import Counter99 from "~/components/generated/counter99";
+import Counter100 from "~/components/generated/counter100";
+import ShowDynamic from "~/components/generated/show-dynamic";
+
+export default component$(() => {
+ return (
+
+
Fallback
+
+ This page has 100 counters. Each counter is a separate component that is
+ preloaded. You can click on the "Say what?" button to see a reprio of
+ the say-what bundle and its transitive dependencies.
+