Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/sixty-rockets-clean.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions packages/qwik-city/src/runtime/src/client-navigate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
26 changes: 20 additions & 6 deletions packages/qwik-city/src/runtime/src/link-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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$<LinkProps>((props) => {
Expand Down Expand Up @@ -51,28 +53,30 @@ export const Link = component$<LinkProps>((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)) {
event.preventDefault();
}
})
: 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' });
Expand All @@ -85,6 +89,11 @@ export const Link = component$<LinkProps>((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
Expand Down Expand Up @@ -115,7 +124,12 @@ export const Link = component$<LinkProps>((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]}
Expand Down
8 changes: 4 additions & 4 deletions packages/qwik-city/src/runtime/src/use-endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ 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,
element: unknown,
opts?: {
action?: RouteActionValue;
clearCache?: boolean;
prefetchSymbols?: boolean;
preloadRouteBundles?: boolean;
isPrefetch?: boolean;
}
) => {
Expand All @@ -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;

Expand Down
Loading