diff --git a/.changeset/sixty-rockets-clean.md b/.changeset/sixty-rockets-clean.md new file mode 100644 index 00000000000..bd0837b179f --- /dev/null +++ b/.changeset/sixty-rockets-clean.md @@ -0,0 +1,5 @@ +--- +'@builder.io/qwik-city': patch +--- + +FEAT: SPA Link navigation now preloads the next route bundles on click with maximum probability, speeding up SPA navigation. diff --git a/packages/qwik-city/src/runtime/src/client-navigate.ts b/packages/qwik-city/src/runtime/src/client-navigate.ts index e43272e1108..5e91e58ca8e 100644 --- a/packages/qwik-city/src/runtime/src/client-navigate.ts +++ b/packages/qwik-city/src/runtime/src/client-navigate.ts @@ -41,10 +41,10 @@ export const newScrollState = (): ScrollState => { }; }; -export const prefetchSymbols = (path: string) => { +export const preloadRouteBundles = (path: string, probability: number = 0.8) => { if (isBrowser) { path = path.endsWith('/') ? path : path + '/'; path = path.length > 1 && path.startsWith('/') ? path.slice(1) : path; - preload(path, 0.8); + preload(path, probability); } }; diff --git a/packages/qwik-city/src/runtime/src/link-component.tsx b/packages/qwik-city/src/runtime/src/link-component.tsx index e2c8a7321b8..9ae80e3e20d 100644 --- a/packages/qwik-city/src/runtime/src/link-component.tsx +++ b/packages/qwik-city/src/runtime/src/link-component.tsx @@ -13,8 +13,10 @@ import { import { getClientNavPath, shouldPreload } from './utils'; import { loadClientData } from './use-endpoint'; import { useLocation, useNavigate } from './use-functions'; -import { prefetchSymbols } from './client-navigate'; +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'; /** @public */ export const Link = component$((props) => { @@ -51,17 +53,18 @@ export const Link = component$((props) => { if (elm && elm.href) { const url = new URL(elm.href); - prefetchSymbols(url.pathname); + preloadRouteBundles(url.pathname); if (elm.hasAttribute('data-prefetch')) { loadClientData(url, elm, { - prefetchSymbols: false, + preloadRouteBundles: false, isPrefetch: true, }); } } }) : undefined; + const preventDefault = clientNavPath ? sync$((event: MouseEvent, target: HTMLAnchorElement) => { if (!(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) { @@ -69,10 +72,11 @@ export const Link = component$((props) => { } }) : undefined; - const handleClick = clientNavPath + + const handleClientSideNavigation = clientNavPath ? $(async (event: Event, elm: HTMLAnchorElement) => { if (event.defaultPrevented) { - // If default was prevented, than it is up to us to make client side navigation. + // If default was prevented, then it is up to us to make client side navigation. if (elm.hasAttribute('q:nbs')) { // Allow bootstrapping into useNavigate. await nav(location.href, { type: 'popstate' }); @@ -85,6 +89,11 @@ export const Link = component$((props) => { }) : undefined; + const handlePreload = $((_: any, elm: HTMLAnchorElement) => { + const url = new URL(elm.href); + preloadRouteBundles(url.pathname, 1); + }); + useVisibleTask$(({ track }) => { track(() => loc.url.pathname); // We need to trigger the onQVisible$ in the visible task for it to fire on subsequent route navigations @@ -115,7 +124,12 @@ export const Link = component$((props) => { // Attr 'q:link' is used as a selector for bootstrapping into spa after context loss {...{ 'q:link': !!clientNavPath }} {...linkProps} - onClick$={[preventDefault, onClick$, handleClick]} + onClick$={[ + preventDefault, + handlePreload, // needs to be in between preventDefault and onClick$ to ensure it starts asap. + onClick$, + handleClientSideNavigation, + ]} data-prefetch={prefetchData} onMouseOver$={[linkProps.onMouseOver$, handlePrefetch]} onFocus$={[linkProps.onFocus$, handlePrefetch]} diff --git a/packages/qwik-city/src/runtime/src/use-endpoint.ts b/packages/qwik-city/src/runtime/src/use-endpoint.ts index 0c36b0524cc..c48528a78f6 100644 --- a/packages/qwik-city/src/runtime/src/use-endpoint.ts +++ b/packages/qwik-city/src/runtime/src/use-endpoint.ts @@ -2,7 +2,7 @@ import { getClientDataPath } from './utils'; import { CLIENT_DATA_CACHE } from './constants'; import type { ClientPageData, RouteActionValue } from './types'; import { _deserializeData } from '@builder.io/qwik'; -import { prefetchSymbols } from './client-navigate'; +import { preloadRouteBundles } from './client-navigate'; export const loadClientData = async ( url: URL, @@ -10,7 +10,7 @@ export const loadClientData = async ( opts?: { action?: RouteActionValue; clearCache?: boolean; - prefetchSymbols?: boolean; + preloadRouteBundles?: boolean; isPrefetch?: boolean; } ) => { @@ -22,8 +22,8 @@ export const loadClientData = async ( qData = CLIENT_DATA_CACHE.get(clientDataPath); } - if (opts?.prefetchSymbols !== false) { - prefetchSymbols(pagePathname); + if (opts?.preloadRouteBundles !== false) { + preloadRouteBundles(pagePathname, 0.8); } let resolveFn: () => void | undefined;