Skip to content

Commit 5c623f5

Browse files
committed
feat(core): dynamic modulepreload
1 parent 752bef7 commit 5c623f5

File tree

17 files changed

+388
-77
lines changed

17 files changed

+388
-77
lines changed

Diff for: packages/docs/src/entry.ssr.tsx

+8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ import Root from './root';
55
export default function (opts: RenderToStreamOptions) {
66
return renderToStream(<Root />, {
77
manifest,
8+
// TODO make this the default
9+
prefetchStrategy: {
10+
implementation: {
11+
linkInsert: 'html-append',
12+
linkRel: 'modulepreload',
13+
prefetchEvent: null,
14+
},
15+
},
816
qwikLoader: {
917
// The docs can be long so make sure to intercept events before the end of the document.
1018
position: 'top',

Diff for: packages/docs/src/root.tsx

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { component$, useContextProvider, useStore } from '@builder.io/qwik';
2-
import { QwikCityProvider, RouterOutlet, ServiceWorkerRegister } from '@builder.io/qwik-city';
2+
import { QwikCityProvider, RouterOutlet } from '@builder.io/qwik-city';
3+
import { Insights } from '@builder.io/qwik-labs';
34
import RealMetricsOptimization from './components/real-metrics-optimization/real-metrics-optimization';
45
import { RouterHead } from './components/router-head/router-head';
6+
import { BUILDER_PUBLIC_API_KEY } from './constants';
57
import { GlobalStore, type SiteStore } from './context';
68
import './global.css';
7-
import { BUILDER_PUBLIC_API_KEY } from './constants';
8-
import { Insights } from '@builder.io/qwik-labs';
99

1010
export const uwu = /*javascript*/ `
1111
;(function () {
@@ -55,7 +55,6 @@ export default component$(() => {
5555
<meta charset="utf-8" />
5656
<script dangerouslySetInnerHTML={uwu} />
5757
<RouterHead />
58-
<ServiceWorkerRegister />
5958

6059
<script dangerouslySetInnerHTML={`(${collectSymbols})()`} />
6160
<Insights publicApiKey={import.meta.env.PUBLIC_QWIK_INSIGHTS_KEY} />

Diff for: packages/docs/src/routes/api/qwik-server/api.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282
}
8383
],
8484
"kind": "Interface",
85-
"content": "```typescript\nexport interface PrefetchImplementation \n```\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[linkFetchPriority?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n'auto' \\| 'low' \\| 'high' \\| null\n\n\n</td><td>\n\n_(Optional)_ Value of the `<link fetchpriority=\"...\">` attribute when link is used. Defaults to `null` if links are inserted.\n\n\n</td></tr>\n<tr><td>\n\n[linkInsert?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n'js-append' \\| 'html-append' \\| null\n\n\n</td><td>\n\n_(Optional)_ `js-append`<!-- -->: Use JS runtime to create each `<link>` and append to the body.\n\n`html-append`<!-- -->: Render each `<link>` within html, appended at the end of the body.\n\n\n</td></tr>\n<tr><td>\n\n[linkRel?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n'prefetch' \\| 'preload' \\| 'modulepreload' \\| null\n\n\n</td><td>\n\n_(Optional)_ Value of the `<link rel=\"...\">` attribute when link is used. Defaults to `prefetch` if links are inserted.\n\n\n</td></tr>\n<tr><td>\n\n[prefetchEvent?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n'always' \\| null\n\n\n</td><td>\n\n_(Optional)_ Dispatch a `qprefetch` event with detail data containing the bundles that should be prefetched. The event dispatch script will be inlined into the document's HTML so any listeners of this event should already be ready to handle the event.\n\nThis implementation will inject a script similar to:\n\n```\n<script type=\"module\">\n document.dispatchEvent(new CustomEvent(\"qprefetch\", { detail:{ \"bundles\": [...] } }))\n</script>\n```\nBy default, the `prefetchEvent` implementation will be set to `always`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\n[workerFetchInsert?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n'always' \\| 'no-link-support' \\| null\n\n\n</td><td>\n\n_(Optional)_ `always`<!-- -->: Always include the worker fetch JS runtime.\n\n`no-link-support`<!-- -->: Only include the worker fetch JS runtime when the browser doesn't support `<link>` prefetch/preload/modulepreload.\n\n\n</td></tr>\n</tbody></table>",
85+
"content": "```typescript\nexport interface PrefetchImplementation \n```\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[linkFetchPriority?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n'auto' \\| 'low' \\| 'high' \\| null\n\n\n</td><td>\n\n_(Optional)_ Value of the `<link fetchpriority=\"...\">` attribute when link is used. Defaults to `null`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\n[linkInsert?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n'js-append' \\| 'html-append' \\| null\n\n\n</td><td>\n\n_(Optional)_ `js-append`<!-- -->: Use JS runtime to create each `<link>` and append to the body.\n\n`html-append`<!-- -->: Render each `<link>` within html, appended at the end of the body.\n\nDefaults to `html-append`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\n[linkRel?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n'prefetch' \\| 'preload' \\| 'modulepreload' \\| null\n\n\n</td><td>\n\n_(Optional)_ Value of the `<link rel=\"...\">` attribute when link is used. Defaults to `modulepreload`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\n[prefetchEvent?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n'always' \\| null\n\n\n</td><td>\n\n_(Optional)_ Dispatch a `qprefetch` event with detail data containing the bundles that should be prefetched. The event dispatch script will be inlined into the document's HTML so any listeners of this event should already be ready to handle the event.\n\nThis implementation will inject a script similar to:\n\n```\n<script type=\"module\">\n document.dispatchEvent(new CustomEvent(\"qprefetch\", { detail:{ \"bundles\": [...] } }))\n</script>\n```\nBy default, the `prefetchEvent` implementation will be set to `null`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\n[workerFetchInsert?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n'always' \\| 'no-link-support' \\| null\n\n\n</td><td>\n\n_(Optional)_ `always`<!-- -->: Always include the worker fetch JS runtime.\n\n`no-link-support`<!-- -->: Only include the worker fetch JS runtime when the browser doesn't support `<link>` prefetch/preload/modulepreload.\n\nDefaults to `null`<!-- -->.\n\n\n</td></tr>\n</tbody></table>",
8686
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/server/types.ts",
8787
"mdFile": "qwik.prefetchimplementation.md"
8888
},
@@ -96,7 +96,7 @@
9696
}
9797
],
9898
"kind": "Interface",
99-
"content": "```typescript\nexport interface PrefetchResource \n```\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[imports](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[PrefetchResource](#prefetchresource)<!-- -->\\[\\]\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\n[url](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>",
99+
"content": "```typescript\nexport interface PrefetchResource \n```\n\n\n<table><thead><tr><th>\n\nProperty\n\n\n</th><th>\n\nModifiers\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\n[imports](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[PrefetchResource](#prefetchresource)<!-- -->\\[\\]\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\n[priority](#)\n\n\n</td><td>\n\n\n</td><td>\n\nboolean\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\n[url](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n\n</td></tr>\n</tbody></table>",
100100
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/server/types.ts",
101101
"mdFile": "qwik.prefetchresource.md"
102102
},

Diff for: packages/docs/src/routes/api/qwik-server/index.md

+20-3
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ Description
243243

244244
</td><td>
245245

246-
_(Optional)_ Value of the `<link fetchpriority="...">` attribute when link is used. Defaults to `null` if links are inserted.
246+
_(Optional)_ Value of the `<link fetchpriority="...">` attribute when link is used. Defaults to `null`.
247247

248248
</td></tr>
249249
<tr><td>
@@ -262,6 +262,8 @@ _(Optional)_ `js-append`: Use JS runtime to create each `<link>` and append to t
262262

263263
`html-append`: Render each `<link>` within html, appended at the end of the body.
264264

265+
Defaults to `html-append`.
266+
265267
</td></tr>
266268
<tr><td>
267269

@@ -275,7 +277,7 @@ _(Optional)_ `js-append`: Use JS runtime to create each `<link>` and append to t
275277

276278
</td><td>
277279

278-
_(Optional)_ Value of the `<link rel="...">` attribute when link is used. Defaults to `prefetch` if links are inserted.
280+
_(Optional)_ Value of the `<link rel="...">` attribute when link is used. Defaults to `modulepreload`.
279281

280282
</td></tr>
281283
<tr><td>
@@ -300,7 +302,7 @@ This implementation will inject a script similar to:
300302
</script>
301303
```
302304

303-
By default, the `prefetchEvent` implementation will be set to `always`.
305+
By default, the `prefetchEvent` implementation will be set to `null`.
304306

305307
</td></tr>
306308
<tr><td>
@@ -319,6 +321,8 @@ _(Optional)_ `always`: Always include the worker fetch JS runtime.
319321

320322
`no-link-support`: Only include the worker fetch JS runtime when the browser doesn't support `<link>` prefetch/preload/modulepreload.
321323

324+
Defaults to `null`.
325+
322326
</td></tr>
323327
</tbody></table>
324328

@@ -362,6 +366,19 @@ Description
362366
</td></tr>
363367
<tr><td>
364368

369+
[priority](#)
370+
371+
</td><td>
372+
373+
</td><td>
374+
375+
boolean
376+
377+
</td><td>
378+
379+
</td></tr>
380+
<tr><td>
381+
365382
[url](#)
366383

367384
</td><td>

Diff for: packages/qwik/src/core/qrl/preload.ts

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/**
2+
* Here we handle preloading of chunks.
3+
*
4+
* Given a symbol hash (in fact any string), we can find all the chunks that it depends on, via the
5+
* bundle graph. We then generate preload link tags for each of those chunks.
6+
*
7+
* The priority is set to high for direct imports and low for indirect imports.
8+
*
9+
* There are several parts to this:
10+
*
11+
* - Load the bundle graph from the preload link tag that was injected during SSR
12+
* - Given a string, find all the chunks that it depends on
13+
* - Generate the preload link tags if needed
14+
*/
15+
16+
import { isDev } from '@builder.io/qwik/build';
17+
import type { QwikBundleGraph } from '../../optimizer/src/types';
18+
import { QBaseAttr, QManifestHash } from '../util/markers';
19+
20+
import { QContainerSelector } from '../util/markers';
21+
22+
let bundlesP: Promise<void> | undefined;
23+
enum BundleImportState {
24+
None,
25+
Low,
26+
Loading,
27+
}
28+
type BundleImport = {
29+
$url$: string | null;
30+
$state$: BundleImportState;
31+
$imports$: string[];
32+
$dynamicImports$: string[];
33+
};
34+
let bundles: Map<string, BundleImport> | undefined;
35+
type WantedBundle = {
36+
name: string;
37+
priority: boolean;
38+
};
39+
const wantedBundles: Set<WantedBundle> = new Set();
40+
41+
const parseBundleGraph = (text: string, base: string) => {
42+
try {
43+
const graph = JSON.parse(text) as QwikBundleGraph;
44+
bundles ||= new Map<string, BundleImport>();
45+
let i = 0;
46+
while (i < graph.length) {
47+
const name = graph[i++] as string;
48+
const url = name.endsWith('.js') ? `${base}${name}` : null;
49+
const imports: string[] = [];
50+
const dynamicImports: string[] = [];
51+
let idx: number | string;
52+
let collection = imports;
53+
while (((idx = graph[i]), typeof idx === 'number')) {
54+
if (idx === -1) {
55+
collection = dynamicImports;
56+
} else {
57+
collection.push(graph[idx] as string);
58+
}
59+
i++;
60+
}
61+
bundles.set(name, {
62+
$url$: url,
63+
$state$: BundleImportState.None,
64+
$imports$: imports,
65+
$dynamicImports$: dynamicImports,
66+
});
67+
}
68+
for (const { name, priority } of wantedBundles) {
69+
preload(name, priority);
70+
}
71+
wantedBundles.clear();
72+
} catch (e) {
73+
console.error('Error parsing bundle graph', e, text);
74+
throw e;
75+
}
76+
};
77+
78+
export const loadBundleGraph = (element: Element) => {
79+
if (typeof window === 'undefined' || bundlesP) {
80+
return;
81+
}
82+
const container = element.closest(QContainerSelector);
83+
if (!container) {
84+
return;
85+
}
86+
const hash = container.getAttribute(QManifestHash);
87+
const base = container.getAttribute(QBaseAttr) || '/';
88+
const link = hash && (container.querySelector(`link#qwik-bg-${hash}`) as HTMLLinkElement | null);
89+
if (!link) {
90+
bundlesP = Promise.reject('No preload link found');
91+
// prevent uncaught promise rejection
92+
bundlesP.catch(() => {});
93+
return;
94+
}
95+
bundlesP = fetch(link.href)
96+
.then((res) => res.text())
97+
.then((text) => parseBundleGraph(text, base))
98+
.catch((e) => {
99+
console.error('Error loading bundle graph, retrying later', e);
100+
setTimeout(() => {
101+
bundlesP = undefined;
102+
}, 60000);
103+
});
104+
};
105+
106+
const makePreloadLink = (bundle: BundleImport, priority: boolean) => {
107+
const link = document.createElement('link');
108+
link.rel = 'modulepreload';
109+
link.href = bundle.$url$!;
110+
link.fetchPriority = priority ? 'high' : 'low';
111+
link.as = 'script';
112+
document.head.appendChild(link);
113+
};
114+
115+
const prioritizeLink = (url: string) => {
116+
const link = document.querySelector(`link[href="${url}"]`) as HTMLLinkElement | null;
117+
if (link) {
118+
link.fetchPriority = 'high';
119+
} else {
120+
console.warn(`Preload link ${url} not found`);
121+
}
122+
};
123+
124+
const preloadBundle = (bundle: BundleImport, priority: boolean) => {
125+
if (bundle.$state$ >= BundleImportState.Loading) {
126+
return;
127+
}
128+
if (bundle.$url$) {
129+
if (bundle.$state$ === BundleImportState.None) {
130+
makePreloadLink(bundle, priority);
131+
} else if (priority && bundle.$state$ === BundleImportState.Low) {
132+
prioritizeLink(bundle.$url$!);
133+
} else {
134+
return;
135+
}
136+
}
137+
bundle.$state$ = priority ? BundleImportState.Loading : BundleImportState.Low;
138+
};
139+
140+
export const preload = (name: string, priority: boolean) => {
141+
if (!bundles) {
142+
wantedBundles.add({ name, priority });
143+
return;
144+
}
145+
const bundle = bundles.get(name);
146+
if (!bundle) {
147+
isDev && console.warn(`Bundle ${name} not found`);
148+
return;
149+
}
150+
const isReprioritize = priority && bundle.$state$ === BundleImportState.Low;
151+
if (bundle.$state$ !== BundleImportState.None && !isReprioritize) {
152+
// prevent loops
153+
return;
154+
}
155+
preloadBundle(bundle, priority);
156+
for (const importName of bundle.$imports$) {
157+
preload(importName, priority);
158+
}
159+
if (!isReprioritize) {
160+
for (const importName of bundle.$dynamicImports$) {
161+
preload(importName, false);
162+
}
163+
}
164+
};

Diff for: packages/qwik/src/core/qrl/qrl-class.ts

+6
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { getQFuncs, QInstance } from '../util/markers';
1515
import { isPromise, maybeThen } from '../util/promises';
1616
import { qDev, qSerialize, qTest, seal } from '../util/qdev';
1717
import { isArray, isFunction, type ValueOrPromise } from '../util/types';
18+
import { loadBundleGraph, preload } from './preload';
1819
import type { QRLDev } from './qrl';
1920
import type { QRL, QrlArgs, QrlReturn } from './qrl.public';
2021

@@ -89,6 +90,10 @@ export const createQRL = <TYPE>(
8990
if (!_containerEl) {
9091
_containerEl = el;
9192
}
93+
// try every time just in case
94+
if (el) {
95+
loadBundleGraph(el);
96+
}
9297
return _containerEl;
9398
};
9499

@@ -221,6 +226,7 @@ export const createQRL = <TYPE>(
221226
if (qDev) {
222227
seal(qrl);
223228
}
229+
preload(hash, true);
224230
return qrl;
225231
};
226232

Diff for: packages/qwik/src/core/util/markers.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const getQFuncs = (document: Document, hash: string): Function[] => {
2929

3030
export const QLocaleAttr = 'q:locale';
3131
export const QContainerAttr = 'q:container';
32-
32+
export const QBaseAttr = 'q:base';
3333
export const QContainerSelector = '[q\\:container]';
3434

3535
export const ResourceEvent = 'qResource';

Diff for: packages/qwik/src/optimizer/src/plugins/vite.ts

+32-14
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,20 @@ function absolutePathAwareJoin(path: Path, ...segments: string[]): string {
11361136
return path.join(...segments);
11371137
}
11381138

1139+
const dynamicTag = '<dynamic>';
1140+
/**
1141+
* This creates a compact array of dependencies for each bundle. It also contains the symbols. The
1142+
* format is:
1143+
*
1144+
* ```
1145+
* [...(bundleName: string, ...directImports: index[], ...dynamicImports: [-1, ...index[]] | [])]
1146+
* ```
1147+
*
1148+
* (index is the position of the dependency in the bundleGraph array)
1149+
*
1150+
* This format allows any string to denote a set of dependencies, which is useful for symbols and
1151+
* SPA paths.
1152+
*/
11391153
export function convertManifestToBundleGraph(manifest: QwikManifest): QwikBundleGraph {
11401154
const bundleGraph: QwikBundleGraph = [];
11411155
const graph = manifest.bundles;
@@ -1171,19 +1185,12 @@ export function convertManifestToBundleGraph(manifest: QwikManifest): QwikBundle
11711185
}
11721186
clearTransitiveDeps(deps, new Set(), depName);
11731187
}
1174-
let didAdd = false;
1175-
for (const depName of bundle.dynamicImports || []) {
1176-
// If we dynamically import a qrl segment that is not a handler, we'll probably need it soon
1177-
const dep = graph[depName];
1178-
if (!graph[depName]) {
1179-
// external dependency
1180-
continue;
1181-
}
1182-
if (dep.isTask) {
1183-
if (!didAdd) {
1184-
deps.add('<dynamic>');
1185-
didAdd = true;
1186-
}
1188+
const internalDynamicImports = bundle.dynamicImports?.filter((d) => graph[d]) || [];
1189+
// If we have a lot of dynamic imports, we don't know which ones are needed, so we don't add any
1190+
// This can happen with registry bundles like for routing
1191+
if (internalDynamicImports.length > 0 && internalDynamicImports.length < 10) {
1192+
deps.add(dynamicTag);
1193+
for (const depName of internalDynamicImports) {
11871194
deps.add(depName);
11881195
}
11891196
}
@@ -1193,6 +1200,17 @@ export function convertManifestToBundleGraph(manifest: QwikManifest): QwikBundle
11931200
bundleGraph.push(null!);
11941201
}
11951202
}
1203+
// Add the symbols to the bundle graph
1204+
for (const [symbol, chunkname] of Object.entries(manifest.mapping)) {
1205+
const bundle = map.get(chunkname);
1206+
if (!bundle) {
1207+
console.warn(`Chunk ${chunkname} for symbol ${symbol} not found in the bundle graph.`);
1208+
} else {
1209+
const idx = symbol.lastIndexOf('_');
1210+
const hash = idx === -1 ? symbol : symbol.slice(idx + 1);
1211+
bundleGraph.push(hash, bundle.index);
1212+
}
1213+
}
11961214
// Second pass to to update dependency pointers
11971215
for (const bundleName of names) {
11981216
const bundle = map.get(bundleName);
@@ -1204,7 +1222,7 @@ export function convertManifestToBundleGraph(manifest: QwikManifest): QwikBundle
12041222
let { index, deps } = bundle;
12051223
index++;
12061224
for (const depName of deps) {
1207-
if (depName === '<dynamic>') {
1225+
if (depName === dynamicTag) {
12081226
bundleGraph[index++] = -1;
12091227
continue;
12101228
}

0 commit comments

Comments
 (0)