Skip to content

Commit 52d81be

Browse files
authored
Merge pull request #7613 from QwikDev/qwikloader-chunk
perf(qwikloader): put qwikloader in a separate bundle - move qwikloader to its own eagerly loaded chunk - automatically unregister events that have no listener attributes - optimize compressed qwikloader size We were embedding the qwikloader, which is faster for initial load, but recent experiments with modulepreload show that this isn't necessary. By putting the qwikloader in its own bundle and using modulepreload + async module script: - the bundle is discovered during the html download - the bundle downloads together with the html - the bundle can be served with brotli compression, which is more costly to do during SSR - subsequent SSR requests never have to download the 1.5-2kb again - multiple containers with the same qwik version can all embed the script import tag and it will only be loaded once This doesn't seem to impact LCP at all. In the pathological 3G simulation with the fixed 2s lag, the chunk is fetched first, while the html is still downloading, and finishes pretty soon after html finished downloading and while the other resources are also downloading, so the user doesn't really notice anything.
2 parents cf3a8aa + 61213c5 commit 52d81be

File tree

18 files changed

+359
-290
lines changed

18 files changed

+359
-290
lines changed

.changeset/empty-sites-tie.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@builder.io/qwik': patch
3+
---
4+
5+
:zap: the qwikloader is no longer embedded in the SSR results. Instead, the same techniques are used as for the preloader to ensure that the qwikloader is active as soon as possible, loaded from a separate bundle. This reduces SSR page size by several kB end ensures that subsequent qwikloader loads are nearly instant.

packages/docs/src/entry.ssr.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,6 @@ export default function (opts: RenderToStreamOptions) {
3434
}
3535
}
3636
return renderToStream(<Root />, {
37-
qwikLoader: {
38-
// The docs can be long so make sure to intercept events before the end of the document.
39-
position: 'top',
40-
},
4137
...opts,
4238
containerAttributes: {
4339
lang: 'en',

packages/docs/src/repl/bundled.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import qCoreMinMjs from '../../node_modules/@builder.io/qwik/dist/core.min.mjs?r
1515
import qCoreMjs from '../../node_modules/@builder.io/qwik/dist/core.mjs?raw-source';
1616
import qOptimizerCjs from '../../node_modules/@builder.io/qwik/dist/optimizer.cjs?raw-source';
1717
import qPreloaderMjs from '../../node_modules/@builder.io/qwik/dist/preloader.mjs?raw-source';
18+
// we use the debug version for the repl so it's understandable
19+
import qQwikLoaderJs from '../../node_modules/@builder.io/qwik/dist/qwikloader.debug.js?raw-source';
1820
import qServerCjs from '../../node_modules/@builder.io/qwik/dist/server.cjs?raw-source';
1921
import qServerDts from '../../node_modules/@builder.io/qwik/dist/server.d.ts?raw-source';
2022
import qWasmCjs from '../../node_modules/@builder.io/qwik/bindings/qwik.wasm.cjs?raw-source';
@@ -57,6 +59,7 @@ export const bundled: PkgUrls = {
5759
'/dist/server.cjs': qServerCjs,
5860
'/dist/server.d.ts': qServerDts,
5961
'/dist/preloader.mjs': qPreloaderMjs,
62+
'/dist/qwikloader.js': qQwikLoaderJs,
6063
'/bindings/qwik.wasm.cjs': qWasmCjs,
6164
'/bindings/qwik_wasm_bg.wasm': qWasmBinUrl,
6265
},

packages/docs/src/repl/worker/repl-plugins.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,13 @@ export const replResolver = (options: ReplInputOptions, buildMode: 'client' | 's
7878
return rsp.text();
7979
}
8080
}
81-
81+
// this id is unchanged because it's an entry point
82+
if (id === '@builder.io/qwik/qwikloader.js') {
83+
const rsp = await depResponse('@builder.io/qwik', '/qwikloader.js');
84+
if (rsp) {
85+
return rsp.text();
86+
}
87+
}
8288
// We're the fallback, we know all the files
8389
if (/\.[jt]sx?$/.test(id)) {
8490
throw new Error(`load: unknown module ${id}`);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@
465465
}
466466
],
467467
"kind": "Interface",
468-
"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[assets?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ \\[fileName: string\\]: [QwikAsset](#qwikasset)<!-- -->; }\n\n\n</td><td>\n\n_(Optional)_ All assets. The key is the fileName relative to the rootDir\n\n\n</td></tr>\n<tr><td>\n\n[bundleGraph?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[QwikBundleGraph](#qwikbundlegraph)\n\n\n</td><td>\n\n_(Optional)_ All bundles in a compact graph format with probabilities\n\n\n</td></tr>\n<tr><td>\n\n[bundleGraphAsset?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n_(Optional)_ The bundle graph fileName\n\n\n</td></tr>\n<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. The key is the bundle fileName relative to \"build/\"\n\n\n</td></tr>\n<tr><td>\n\n[core?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n_(Optional)_ The Qwik core bundle fileName\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. The key is the symbol name, the value is the bundle fileName\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?: { type: [EntryStrategy](#entrystrategy)<!-- -->\\['type'\\]; }; }\n\n\n</td><td>\n\n_(Optional)_ The options used to build the manifest\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)_ The platform used to build the manifest\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 fileName\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\nThe version of the manifest\n\n\n</td></tr>\n</tbody></table>",
468+
"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[assets?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n{ \\[fileName: string\\]: [QwikAsset](#qwikasset)<!-- -->; }\n\n\n</td><td>\n\n_(Optional)_ All assets. The key is the fileName relative to the rootDir\n\n\n</td></tr>\n<tr><td>\n\n[bundleGraph?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n[QwikBundleGraph](#qwikbundlegraph)\n\n\n</td><td>\n\n_(Optional)_ All bundles in a compact graph format with probabilities\n\n\n</td></tr>\n<tr><td>\n\n[bundleGraphAsset?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n_(Optional)_ The bundle graph fileName\n\n\n</td></tr>\n<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. The key is the bundle fileName relative to \"build/\"\n\n\n</td></tr>\n<tr><td>\n\n[core?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n_(Optional)_ The Qwik core bundle fileName\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. The key is the symbol name, the value is the bundle fileName\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?: { type: [EntryStrategy](#entrystrategy)<!-- -->\\['type'\\]; }; }\n\n\n</td><td>\n\n_(Optional)_ The options used to build the manifest\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)_ The platform used to build the manifest\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 fileName\n\n\n</td></tr>\n<tr><td>\n\n[qwikLoader?](#)\n\n\n</td><td>\n\n\n</td><td>\n\nstring\n\n\n</td><td>\n\n_(Optional)_ The Qwik loader bundle fileName\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\nThe version of the manifest\n\n\n</td></tr>\n</tbody></table>",
469469
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts",
470470
"mdFile": "qwik.qwikmanifest.md"
471471
},
@@ -667,7 +667,7 @@
667667
}
668668
],
669669
"kind": "TypeAlias",
670-
"content": "The manifest values that are needed for SSR.\n\n\n```typescript\nexport type ServerQwikManifest = Pick<QwikManifest, 'manifestHash' | 'injections' | 'bundleGraph' | 'bundleGraphAsset' | 'mapping' | 'preloader' | 'core'>;\n```\n**References:** [QwikManifest](#qwikmanifest)",
670+
"content": "The manifest values that are needed for SSR.\n\n\n```typescript\nexport type ServerQwikManifest = Pick<QwikManifest, 'manifestHash' | 'injections' | 'bundleGraph' | 'bundleGraphAsset' | 'mapping' | 'preloader' | 'core' | 'qwikLoader'>;\n```\n**References:** [QwikManifest](#qwikmanifest)",
671671
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/optimizer/src/types.ts",
672672
"mdFile": "qwik.serverqwikmanifest.md"
673673
},

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1640,6 +1640,21 @@ _(Optional)_ The preloader bundle fileName
16401640
</td></tr>
16411641
<tr><td>
16421642

1643+
[qwikLoader?](#)
1644+
1645+
</td><td>
1646+
1647+
</td><td>
1648+
1649+
string
1650+
1651+
</td><td>
1652+
1653+
_(Optional)_ The Qwik loader bundle fileName
1654+
1655+
</td></tr>
1656+
<tr><td>
1657+
16431658
[symbols](#)
16441659

16451660
</td><td>
@@ -2764,6 +2779,7 @@ export type ServerQwikManifest = Pick<
27642779
| "mapping"
27652780
| "preloader"
27662781
| "core"
2782+
| "qwikLoader"
27672783
>;
27682784
```
27692785

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@
138138
}
139139
],
140140
"kind": "Interface",
141-
"content": "```typescript\nexport interface QwikLoaderOptions \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[include?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n'always' \\| 'never' \\| 'auto'\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n<tr><td>\n\n[position?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n'top' \\| 'bottom'\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n</tbody></table>",
141+
"content": "```typescript\nexport interface QwikLoaderOptions \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[include?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n'always' \\| 'never' \\| 'auto'\n\n\n</td><td>\n\n_(Optional)_ Whether to include the qwikloader script in the document. Normally you don't need to worry about this, but in case of multi-container apps using different Qwik versions, you might want to only enable it on one of the containers.\n\nDefaults to `'auto'`<!-- -->.\n\n\n</td></tr>\n<tr><td>\n\n[position?](#)\n\n\n</td><td>\n\n\n</td><td>\n\n'top' \\| 'bottom'\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n</tbody></table>",
142142
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/server/types.ts",
143143
"mdFile": "qwik.qwikloaderoptions.md"
144144
},

packages/docs/src/routes/api/qwik-server/index.mdx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,9 @@ Description
575575

576576
</td><td>
577577

578-
_(Optional)_
578+
_(Optional)_ Whether to include the qwikloader script in the document. Normally you don't need to worry about this, but in case of multi-container apps using different Qwik versions, you might want to only enable it on one of the containers.
579+
580+
Defaults to `'auto'`.
579581

580582
</td></tr>
581583
<tr><td>

packages/qwik/src/optimizer/src/manifest.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,10 @@ export function generateManifestFromBundles(
486486
manifest.preloader = bundleFileName;
487487
} else if (modulePaths.some((m) => /[/\\]qwik[/\\]dist[/\\]core\.[^/]*js$/.test(m))) {
488488
manifest.core = bundleFileName;
489+
} else if (
490+
modulePaths.some((m) => /[/\\]qwik[/\\]dist[/\\]qwikloader(\.debug)?\.[^/]*js$/.test(m))
491+
) {
492+
manifest.qwikLoader = bundleFileName;
489493
}
490494
}
491495

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

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,19 @@ export function createQwikPlugin(optimizerOptions: OptimizerOptions = {}) {
407407
debug(`transformedOutputs.clear()`);
408408
clientTransformedOutputs.clear();
409409
serverTransformedOutputs.clear();
410+
411+
if (opts.target === 'client') {
412+
const ql = await _ctx.resolve('@builder.io/qwik/qwikloader.js', undefined, {
413+
skipSelf: true,
414+
});
415+
if (ql) {
416+
_ctx.emitFile({
417+
id: ql.id,
418+
type: 'chunk',
419+
preserveSignature: 'allow-extension',
420+
});
421+
}
422+
}
410423
};
411424

412425
const getIsServer = (viteOpts?: { ssr?: boolean }) => {
@@ -880,12 +893,13 @@ export const isDev = ${JSON.stringify(isDev)};
880893
if (manifest?.manifestHash) {
881894
serverManifest = {
882895
manifestHash: manifest.manifestHash,
883-
injections: manifest.injections,
884-
bundleGraph: manifest.bundleGraph,
885-
mapping: manifest.mapping,
886-
preloader: manifest.preloader,
887896
core: manifest.core,
897+
preloader: manifest.preloader,
898+
qwikLoader: manifest.qwikLoader,
888899
bundleGraphAsset: manifest.bundleGraphAsset,
900+
injections: manifest.injections,
901+
mapping: manifest.mapping,
902+
bundleGraph: manifest.bundleGraph,
889903
};
890904
}
891905
return `// @qwik-client-manifest
@@ -924,11 +938,12 @@ export const manifest = ${JSON.stringify(serverManifest)};\n`;
924938
function manualChunks(id: string, { getModuleInfo }: Rollup.ManualChunkMeta) {
925939
// The preloader has to stay in a separate chunk if it's a client build
926940
// the vite preload helper must be included or to prevent breaking circular dependencies
927-
if (
928-
opts.target === 'client' &&
929-
(id.endsWith(QWIK_PRELOADER_REAL_ID) || id === '\0vite/preload-helper.js')
930-
) {
931-
return 'qwik-preloader';
941+
if (opts.target === 'client') {
942+
if (id.endsWith(QWIK_PRELOADER_REAL_ID) || id === '\0vite/preload-helper.js') {
943+
return 'qwik-preloader';
944+
} else if (/qwik[\\/]dist[\\/]qwikloader\.js$/.test(id)) {
945+
return 'qwik-loader';
946+
}
932947
}
933948

934949
const module = getModuleInfo(id)!;

0 commit comments

Comments
 (0)