Skip to content

Commit d6e5953

Browse files
committed
wip: preloader - needs cleanup and tuning
1 parent 28c6236 commit d6e5953

File tree

22 files changed

+415
-228
lines changed

22 files changed

+415
-228
lines changed

packages/docs/src/routes/api/qwik-optimizer/api.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@
434434
}
435435
],
436436
"kind": "Interface",
437-
"content": "The metadata of the build. One of its uses is storing where QRL symbols are located.\n\n\n```typescript\nexport interface QwikManifest \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[bundles](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ \\[fileName: string\\]: [QwikBundle](#qwikbundle)<!-- -->; }\n\n\n</td><td>\n\nAll code bundles, used to know the import graph\n\n\n</td></tr>\n<tr><td>\n\n[injections?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[GlobalInjections](#globalinjections)<!-- -->\\[\\]\n\n\n</td><td>\n\n_(Optional)_ CSS etc to inject in the document head\n\n\n</td></tr>\n<tr><td>\n\n[manifestHash](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\nContent hash of the manifest, if this changes, the code changed\n\n\n</td></tr>\n<tr><td>\n\n[mapping](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ \\[symbolName: string\\]: string; }\n\n\n</td><td>\n\nWhere QRLs are located\n\n\n</td></tr>\n<tr><td>\n\n[options?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ target?: string; buildMode?: string; entryStrategy?: { \\[key: string\\]: any; }; }\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[platform?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ \\[name: string\\]: string; }\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[symbols](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ \\[symbolName: string\\]: [QwikSymbol](#qwiksymbol)<!-- -->; }\n\n\n</td><td>\n\nQRL symbols\n\n\n</td></tr>\n<tr><td>\n\n[version](#)\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>",
437+
"content": "The metadata of the build. One of its uses is storing where QRL symbols are located.\n\n\n```typescript\nexport interface QwikManifest \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[bundles](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ \\[fileName: string\\]: [QwikBundle](#qwikbundle)<!-- -->; }\n\n\n</td><td>\n\nAll code bundles, used to know the import graph\n\n\n</td></tr>\n<tr><td>\n\n[injections?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[GlobalInjections](#globalinjections)<!-- -->\\[\\]\n\n\n</td><td>\n\n_(Optional)_ CSS etc to inject in the document head\n\n\n</td></tr>\n<tr><td>\n\n[manifestHash](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\nContent hash of the manifest, if this changes, the code changed\n\n\n</td></tr>\n<tr><td>\n\n[mapping](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ \\[symbolName: string\\]: string; }\n\n\n</td><td>\n\nWhere QRLs are located\n\n\n</td></tr>\n<tr><td>\n\n[options?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ target?: string; buildMode?: string; entryStrategy?: { \\[key: string\\]: any; }; }\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[platform?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ \\[name: string\\]: string; }\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[preloader?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n_(Optional)_ The preloader bundle\n\n\n</td></tr>\n<tr><td>\n\n[symbols](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ \\[symbolName: string\\]: [QwikSymbol](#qwiksymbol)<!-- -->; }\n\n\n</td><td>\n\nQRL symbols\n\n\n</td></tr>\n<tr><td>\n\n[version](#)\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>",
438438
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts",
439439
"mdFile": "qwik.qwikmanifest.md"
440440
},

packages/docs/src/routes/api/qwik-optimizer/index.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1476,6 +1476,21 @@ _(Optional)_
14761476
</td></tr>
14771477
<tr><td>
14781478

1479+
[preloader?](#)
1480+
1481+
</td><td>
1482+
1483+
</td><td>
1484+
1485+
string
1486+
1487+
</td><td>
1488+
1489+
_(Optional)_ The preloader bundle
1490+
1491+
</td></tr>
1492+
<tr><td>
1493+
14791494
[symbols](#)
14801495

14811496
</td><td>

packages/qwik-city/src/runtime/src/client-navigate.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { isBrowser, _preload } from '@builder.io/qwik';
1+
import { isBrowser } from '@builder.io/qwik';
2+
// @ts-expect-error we don't have types for the preloader yet
3+
import { p as preload } from '@builder.io/qwik/preloader';
24
import type { NavigationType, ScrollState } from './types';
35
import { isSamePath, toPath } from './utils';
46

@@ -43,6 +45,6 @@ export const prefetchSymbols = (path: string) => {
4345
if (isBrowser) {
4446
path = path.endsWith('/') ? path : path + '/';
4547
path = path.length > 1 && path.startsWith('/') ? path.slice(1) : path;
46-
_preload(path, true);
48+
preload(path);
4749
}
4850
};

packages/qwik/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@
109109
"import": "./dist/optimizer.mjs",
110110
"require": "./dist/optimizer.cjs"
111111
},
112+
"./preloader": {
113+
"import": "./dist/preloader.mjs"
114+
},
112115
"./server.cjs": "./dist/server.cjs",
113116
"./server.mjs": "./dist/server.mjs",
114117
"./server": {

packages/qwik/src/core/api.md

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -624,9 +624,6 @@ export const PrefetchServiceWorker: (opts: {
624624
nonce?: string;
625625
}) => JSXNode_2<'script'>;
626626

627-
// @internal (undocumented)
628-
export const _preload: (name: string, priority: boolean) => void;
629-
630627
// @public (undocumented)
631628
export interface ProgressHTMLAttributes<T extends Element> extends Attrs<'progress', T> {
632629
}

packages/qwik/src/core/internal.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,3 @@ export {
1616
} from './use/use-core';
1717
export { _jsxQ, _jsxC, _jsxS } from './render/jsx/jsx-runtime';
1818
export { _fnSignal } from './qrl/inlined-fn';
19-
export { preload as _preload } from './qrl/preload';
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/**
2+
* Note: this file gets built separately from the rest of the core module, and is then kept separate
3+
* in the dist directory via manualChunks. This way it can run before the rest of the core module is
4+
* loaded, but core can still use it.
5+
*
6+
* Here we handle preloading of chunks.
7+
*
8+
* Given a symbol hash (in fact any string), we can find all the chunks that it depends on, via the
9+
* bundle graph. We then generate preload link tags for each of those chunks.
10+
*
11+
* The priority is set to high for direct imports and low for indirect imports.
12+
*
13+
* There are several parts to this:
14+
*
15+
* - Load the bundle graph from the preload link tag that was injected during SSR
16+
* - Given a string, find all the chunks that it depends on
17+
* - Generate the preload link tags if needed
18+
*/
19+
/**
20+
* Todo
21+
*
22+
* - High and low priority sets
23+
* - Num preloads active at a time
24+
* - If no modulepreload support, do 1 fetch at a time
25+
*/
26+
27+
import { isBrowser } from '@builder.io/qwik/build';
28+
import type { QwikBundleGraph } from '../optimizer/src/types';
29+
30+
const enum BundleImportState {
31+
None,
32+
Low,
33+
Queued,
34+
Loading,
35+
}
36+
type BundleImport = {
37+
$url$: string | null;
38+
$state$: BundleImportState;
39+
$imports$: string[];
40+
$dynamicImports$: string[];
41+
};
42+
const bundles = new Map<string, BundleImport>();
43+
const high: BundleImport[] = [];
44+
const low: BundleImport[] = [];
45+
46+
let base: string | undefined;
47+
48+
// minification helpers
49+
const doc = isBrowser ? document : undefined!;
50+
const modulePreloadStr = 'modulepreload';
51+
const preloadStr = 'preload';
52+
53+
const makeBundle = (path: string, imports: string[], dynamicImports: string[]) => {
54+
const url = path.endsWith('.js') ? new URL(`${base}${path}`, doc.baseURI).toString() : null;
55+
return {
56+
$url$: url,
57+
$state$: BundleImportState.None,
58+
$imports$: imports,
59+
$dynamicImports$: dynamicImports,
60+
};
61+
};
62+
63+
/**
64+
* Lazily load the bundle graph and then import dependencies of bundles that were loaded already.
65+
*
66+
* @internal
67+
*/
68+
const loadBundleGraph = (basePath: string, manifestHash: string) => {
69+
if (!isBrowser || base) {
70+
return;
71+
}
72+
base = basePath;
73+
// TODO check TTI, maybe inject fetch link with timeout so we don't do the fetch directly
74+
fetch(`${basePath}q-bundle-graph-${manifestHash}.json`)
75+
.then((res) => res.text())
76+
.then((text) => parseBundleGraph(text, basePath))
77+
.catch(console.error);
78+
};
79+
80+
// TODO fallback to fetch instead of preload?
81+
const rel =
82+
isBrowser && doc.createElement('link').relList.supports(modulePreloadStr)
83+
? modulePreloadStr
84+
: preloadStr;
85+
86+
let highCount = 0;
87+
let lowCount = 0;
88+
const trigger = () => {
89+
while (highCount < 4 && high.length) {
90+
const bundle = high.pop()!;
91+
makePreloadLink(bundle!, true);
92+
}
93+
while (lowCount < 2 && low.length) {
94+
const bundle = low.pop()!;
95+
makePreloadLink(bundle!);
96+
}
97+
};
98+
99+
const makePreloadLink = (bundle: BundleImport, priority?: boolean) => {
100+
if (bundle.$state$ === BundleImportState.Loading) {
101+
return;
102+
}
103+
bundle.$state$ = BundleImportState.Loading;
104+
const link = doc.createElement('link');
105+
link.href = bundle.$url$!;
106+
if (priority) {
107+
highCount++;
108+
link.rel = rel;
109+
} else {
110+
lowCount++;
111+
link.rel = preloadStr;
112+
link.fetchPriority = 'low';
113+
}
114+
link.as = 'script';
115+
link.onload = link.onerror = () => {
116+
link.remove();
117+
if (priority) {
118+
highCount--;
119+
} else {
120+
lowCount--;
121+
}
122+
trigger();
123+
};
124+
125+
doc.head.appendChild(link);
126+
return 1;
127+
};
128+
129+
const preloadBundle = (bundle: BundleImport, priority: boolean) => {
130+
if (bundle.$state$ > BundleImportState.Queued) {
131+
return;
132+
}
133+
if (bundle.$url$) {
134+
(priority ? high : low).unshift(bundle);
135+
}
136+
bundle.$state$ = priority ? BundleImportState.Queued : BundleImportState.Low;
137+
};
138+
139+
/**
140+
* Preload a bundle or bundles. Requires calling loadBundleGraph first.
141+
*
142+
* @internal
143+
*/
144+
const preload = (name: string | string[], priority: boolean, seen?: Set<BundleImport>) => {
145+
if (!isBrowser || !base) {
146+
return;
147+
}
148+
if (Array.isArray(name)) {
149+
name.forEach((n) => preload(n, priority));
150+
return;
151+
}
152+
let bundle = bundles.get(name);
153+
if (!bundle) {
154+
bundle = makeBundle(name, [], []);
155+
bundles.set(name, bundle);
156+
}
157+
if (seen && seen.has(bundle)) {
158+
// prevent loops
159+
return;
160+
}
161+
seen?.add(bundle);
162+
preloadBundle(bundle, priority);
163+
for (const importName of bundle.$imports$) {
164+
preload(importName, priority, seen || new Set([bundle]));
165+
}
166+
for (const importName of bundle.$dynamicImports$) {
167+
preload(importName, false, seen || new Set([bundle]));
168+
}
169+
trigger();
170+
};
171+
172+
const parseBundleGraph = (text: string, base: string) => {
173+
try {
174+
const graph = JSON.parse(text) as QwikBundleGraph;
175+
let i = 0;
176+
const toProcess = [];
177+
while (i < graph.length) {
178+
const name = graph[i++] as string;
179+
const imports: string[] = [];
180+
const dynamicImports: string[] = [];
181+
let idx: number | string;
182+
let collection = imports;
183+
while (((idx = graph[i]), typeof idx === 'number')) {
184+
if (idx === -1) {
185+
collection = dynamicImports;
186+
} else {
187+
collection.push(graph[idx] as string);
188+
}
189+
i++;
190+
}
191+
if (bundles.has(name)) {
192+
const bundle = bundles.get(name)!;
193+
bundle.$imports$ = imports;
194+
bundle.$dynamicImports$ = dynamicImports;
195+
toProcess.push(bundle);
196+
} else {
197+
bundles.set(name, makeBundle(name, imports, dynamicImports));
198+
}
199+
}
200+
for (const bundle of toProcess) {
201+
for (const importName of bundle.$imports$) {
202+
preload(importName, bundle.$state$ !== BundleImportState.Queued);
203+
}
204+
for (const importName of bundle.$dynamicImports$) {
205+
preload(importName, false);
206+
}
207+
}
208+
} catch (e) {
209+
console.error('Error parsing bundle graph', e, text);
210+
throw e;
211+
}
212+
};
213+
214+
// Short names for minification
215+
export { loadBundleGraph as l, preload as p };

0 commit comments

Comments
 (0)