Skip to content

Commit f21f18c

Browse files
committed
wip tweaks
1 parent bbe5ae6 commit f21f18c

File tree

6 files changed

+145
-35
lines changed

6 files changed

+145
-35
lines changed

packages/docs/vite.config.mts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,8 @@ export default defineConfig(async () => {
193193
sourcemap: true,
194194
rollupOptions: {
195195
output: {
196+
// at slow 3G speeds, 4kb takes 150ms to download
197+
experimentalMinChunkSize: 4096,
196198
assetFileNames: 'assets/[hash]-[name].[ext]',
197199
},
198200
external: ['@docsearch/css'],

packages/qwik/src/core/preloader.ts

Lines changed: 18 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,9 @@ const trigger = () => {
9898
preloadOne(bundle!, true);
9999
}
100100
// these are opportunistic
101-
if (!highCount && !lowCount) {
102-
while (lowCount < 6 && low.length) {
103-
const bundle = low.pop()!;
104-
preloadOne(bundle!);
105-
}
101+
while (highCount + lowCount < 6 && low.length) {
102+
const bundle = low.pop()!;
103+
preloadOne(bundle!);
106104
}
107105
if (!high.length && !low.length) {
108106
const loaded = [...bundles.values()].filter((b) => b.$state$ >= BundleImportState.Loading);
@@ -158,10 +156,7 @@ const preloadOne = (bundle: BundleImport, priority?: boolean) => {
158156

159157
bundle.$priority$ ||= priority!;
160158
preload(bundle.$imports$, priority);
161-
if (bundle.$priority$) {
162-
// make sure to queue the high priority imports first so they preloaded before the low priority ones
163-
preload(bundle.$dynamicImports$);
164-
}
159+
preload(bundle.$dynamicImports$);
165160
};
166161

167162
const makeBundle = (path: string, imports: string[], dynamicImports: string[]) => {
@@ -178,7 +173,7 @@ const makeBundle = (path: string, imports: string[], dynamicImports: string[]) =
178173
$loaded$: 0,
179174
};
180175
};
181-
const ensureBundle = (name: string) => {
176+
const ensureBundle = (name: string, collection: BundleImport[]) => {
182177
let bundle = bundles.get(name);
183178
if (!bundle) {
184179
if (gotBundleGraph) {
@@ -190,18 +185,20 @@ const ensureBundle = (name: string) => {
190185
if (checkLoaded(bundle)) {
191186
return;
192187
}
193-
return bundle;
188+
// TEMP we should use a sorted set instead
189+
if (!collection.includes(bundle)) {
190+
return bundle;
191+
}
194192
};
195193

196194
const parseBundleGraph = (text: string) => {
197195
log(`parseBundleGraph ${text.length >> 10}kB`);
198196
const graph = JSON.parse(text) as QwikBundleGraph;
199197
let i = 0;
200198
// All existing loading bundles need imports processed
201-
const toProcess = Object.keys(bundles)
202-
.filter((name) => {
203-
const bundle = bundles.get(name)!;
204-
return bundle.$state$ === BundleImportState.Loading && bundle.$priority$;
199+
const toProcess = [...bundles.values()]
200+
.filter((bundle) => {
201+
return bundle.$state$ >= BundleImportState.Loading && bundle.$priority$;
205202
})
206203
.reverse();
207204
while (i < graph.length) {
@@ -226,12 +223,11 @@ const parseBundleGraph = (text: string) => {
226223
bundles.set(name, makeBundle(name, imports, dynamicImports));
227224
}
228225
}
229-
log(`parseBundleGraph done ${bundles.size} bundles`);
226+
log(`parseBundleGraph done ${bundles.size} bundles, will process ${toProcess.length} bundles`);
230227
gotBundleGraph = true;
231-
for (const name of toProcess) {
232-
const bundle = bundles.get(name)!;
233-
// we assume low priority
234-
preload([...bundle.$imports$, ...bundle.$dynamicImports$]);
228+
for (const bundle of toProcess) {
229+
preload(bundle.$imports$, true);
230+
preload(bundle.$dynamicImports$);
235231
}
236232
};
237233

@@ -246,13 +242,13 @@ const preload = (name: string | string[], priority?: boolean) => {
246242
}
247243
const queue = priority ? high : low;
248244
if (Array.isArray(name)) {
249-
const bundles = name.map(ensureBundle).filter(Boolean) as BundleImport[];
245+
const bundles = name.map((n) => ensureBundle(n, queue)).filter(Boolean) as BundleImport[];
250246
if (!bundles.length) {
251247
return;
252248
}
253249
queue.push(...bundles.reverse());
254250
} else {
255-
const bundle = ensureBundle(name);
251+
const bundle = ensureBundle(name, queue);
256252
if (!bundle) {
257253
return;
258254
}

packages/qwik/src/optimizer/src/plugins/plugin.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -889,11 +889,12 @@ export const manifest = ${JSON.stringify(manifest)};\n`;
889889
}
890890

891891
function manualChunks(id: string, { getModuleInfo }: Rollup.ManualChunkMeta) {
892+
// The preloader has to stay in a separate chunk
893+
if (id.endsWith(QWIK_PRELOADER_REAL_ID)) {
894+
return 'qwik-preloader';
895+
}
896+
892897
if ((opts.entryStrategy as SmartEntryStrategy).manual) {
893-
// The preloader has to stay in a separate chunk
894-
if (id.endsWith(QWIK_PRELOADER_REAL_ID)) {
895-
return 'qwik-preloader';
896-
}
897898
const module = getModuleInfo(id)!;
898899
const segment = module.meta.segment as SegmentAnalysis | undefined;
899900

packages/qwik/src/server/prefetch-implementation.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -157,14 +157,13 @@ function linkJsImplementation(
157157
if (prio.length) {
158158
// We mark all the urls as low priority, so that newly needed resources are loaded first
159159
// We use a Promise so the script doesn't block the initial page load
160-
const script = `
161-
const d=Date.now();console.log('preloader loading',d);
162-
import("${base}${preloadChunk}").then(({l,p})=>{
163-
console.log('preloader start',Date.now()-d);
164-
l(${JSON.stringify(base)},${JSON.stringify(manifestHash)});
165-
p(${JSON.stringify(prio.slice(0, 20))});
166-
})
167-
`.replaceAll(/^\s+|\s*\n/gm, '');
160+
const script =
161+
`const d=Date.now();console.log('preloader loading',d);` +
162+
`import("${base}${preloadChunk}").then(({l,p})=>{` +
163+
(`console.log('preloader start',Date.now()-d);` +
164+
`l(${JSON.stringify(base)},${JSON.stringify(manifestHash)});` +
165+
`p(${JSON.stringify(prio)});`) +
166+
`})`;
168167

169168
// TODO move this to the top of the page
170169
prefetchNodes.push(
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Preloading
2+
3+
(this is wip, need to explain how bundlegraph stores score modifiers and how they are used)
4+
5+
When a user clicks a button, we want the code to be already there.
6+
When they navigate to a new page, we want this to be as fast as possible.
7+
8+
If code is missing, the user has to wait for it to load. Then, its static imports also have to load. More waiting, this is a waterfall.
9+
10+
We could simply downlad all code at start, but on a slow connection or device this can mean that the code that is needed will be loaded last.
11+
12+
We aim to minimize the time it takes for the code to be loaded and executed. For every interaction, there are some loading strategies that result in the shortest wait for the user. We try to find the best strategy for each case.
13+
14+
Users will start to notice a latency in UI response from 200ms. At weak mobile speeds, that translates to about 8kB of data, but there's also the 100-500ms latency of 3G to consider.
15+
16+
The Qwik Docs site has 2.3MB of Brotli-compressed JS. Over a weak mobile connection this easily can take a minute to download. So we need to split the code into bundles.
17+
18+
We need to balance bundle size versus more bundles increasing latency via HTTP request overhead.
19+
20+
## Strategy
21+
22+
### Waterfall prevention
23+
24+
To prevent waterfalls, we need to tell the browser which imports will be needed when an import loads. This is the entire static import graph, all the `import` statements from all the bundles that are loaded.
25+
26+
We can do this because the majority of our imports are QRLs. When a QRL is run, we can use information about the import graph to give the browser the entire list of all imports that need to be loaded.
27+
28+
### Loading code before it is needed
29+
30+
If we have available bandwidth, we can download code before it is needed. We should determine which code is most likely to be needed, and download it first, as well as its static imports.
31+
32+
To know which code is most likely to be needed, we can use the bundle scoring system.
33+
34+
### Bundle scoring system
35+
36+
Each bundle gets a score based on:
37+
38+
- Interactivity: How much impact to interactivity is there if the bundle is missing?
39+
- score 0 to 5
40+
- a click handler is very interactive, but the Task that gets executed by the signal that changes might also be important
41+
- qwik core is not interactive at all, but its importers are
42+
- dependencies get the sum score of their importers
43+
- Size: How much code is there to download?
44+
- score: percentage of total js bundle size, including all static imports and their static imports etc
45+
- heavy bundles that are not interactive should not be preloaded at all, but very interactive bundles should be preloaded before others, so that smaller bundles can download in parallel.
46+
- Likelihood of being needed
47+
- score: chance % of being needed in the next 5 seconds
48+
- this is a guess, based on the type of symbols in the bundle and the type of the bundle
49+
- dependencies get the sum score of their importers
50+
- Insights can be used to improve this guess, but this is only implemented for Link SPA preloading and bundle bundling.
51+
- this also changes during execution: importing one bundle can increase the score of another. The extreme case is direct imports, they are then 100% sure to be needed.
52+
53+
## Available techniques
54+
55+
Note, currently we are only interested in preloading code, not assets. Furthermore, we only target browsers that support ES bundles.
56+
57+
You can preload a bundle either declaratively or imperatively.
58+
59+
### Declarative
60+
61+
By declaring a bundle, you tell the browser to expect running the bundle soon. The browser can then decide what to do with it.
62+
63+
The best way to preload is to use `<link rel="bundlepreload">` tags. This tells the browser to expect running the bundle soon. The browser will then download the bundle in the background, parse it and also download its static imports.
64+
65+
If the browser does not support this, we can use `<link rel="preload">` tags. This is not as good, because the browser will not parse the code.
66+
67+
At the time of writing, `bundlepreload` has 93% support and `preload` has 97% support.
68+
69+
Note that once you add a preload tag, you can't control when the browser will download the bundle. Therefore you should not have too many tags at the same time, and the preloader keeps a queue of bundles to preload. The bundles with high likelihood are allowed to have more tags at the same time than the ones with low likelihood.
70+
71+
### Imperative
72+
73+
We can also simply `fetch` the bundle and discard the response. The browser the hopefully keeps it in cache.
74+
75+
This is unlikely to be optimal because we don't have the same information as the browser about the currently available resources.
76+
77+
It is a possible workaround for when devices don't support bundle preloading. We'll use this if we see a need.
78+
79+
## Implementation
80+
81+
### Bundle graph
82+
83+
Ideally, we have a function that gets the current DOM, the browser position and the user interaction history and returns the likelihoods of the bundles. Perhaps one day we can use a neural network to do this, but for now we use simple heuristics.
84+
85+
For each bundle, we have a list of scoring modifiers for other bundles. These are numbers that are added to the score of the bundle.
86+
87+
We encode the scores in a "bundle graph". This is a compact representation of the known bundles and how they influence the likelihood of other bundles.
88+
89+
### SSR
90+
91+
We have early preloading, by adding `<link rel="bundlepreload">` tags to the SSR response. This should be used only for the bundles that are almost certain to be needed.
92+
93+
Then, we inject a script tag that imports the preloader and passes the list of likely needed bundles to it. This list depends on the SSR result.
94+
95+
Note that the preloader is a small bundle in a separate bundle that is also imported by Qwik itself, so its state is available instantly. Qwik can then request preloading for QRL segments etc. We tell the bundler to make sure to keep the preloader in a separate bundle.
96+
97+
Once the preloader is loaded, it will start downloading the bundles in the background in order of their score. It also downloads the bundle graph so to have a complete picture.
98+
99+
### QRL preloading
100+
101+
When a QRL is created, we tell the preloader about the symbol with low priority.
102+
103+
### Link preloading
104+
105+
When a Link is visible, we can preload the likely bundles of the target page. The scoring could also use the popularity of target page, but this is not implemented yet.

scripts/submodule-preloader.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,18 @@ export async function submodulePreloader(config: BuildConfig) {
3333

3434
// Rename $properties$ to short names but leave the rest legible
3535
// The final app will minify it when needed
36+
3637
const minified = await minify(result.code, {
37-
compress: false,
38+
compress: {
39+
// Trying to eliminate the enum declaration and failing
40+
dead_code: true,
41+
unused: true,
42+
conditionals: true,
43+
},
3844
mangle: {
3945
toplevel: false,
4046
module: false,
47+
keep_fnames: true,
4148
properties: {
4249
regex: '^\\$.+\\$$',
4350
},

0 commit comments

Comments
 (0)