From 1df9ec42b1af6e3b42f61028380a980e3254539f Mon Sep 17 00:00:00 2001 From: botandbang <337765928+botandbang@users.noreply.github.com> Date: Sat, 31 Oct 2026 10:57:19 -0400 Subject: [PATCH] fix(web): preload the root error page so hydration skips a round trip SvelteKit's client awaits the root error page (nodes/1) in start() before it hydrates, but the server only modulepreloads the route's own nodes (sveltejs/kit#17415). So every page fetched nodes/1 only after start.js had run, a round trip behind the largest chunk. Lighthouse's simulated LCP counts that request whenever it finishes before the first paint on localhost, which a busy machine makes likely. That took /projects/halflight from 2.10 s to 2.25 s, 48 ms under the 2.3 s budget, and one more simulated round trip put it over (2,404 and 2,408 ms). With nodes/1 preloaded alongside the other modules it's off the critical path: under 16 busy processes, halflight's 12 runs measured 2,100 to 2,118 ms (before: 2,103 to 2,256 ms, 8 of 12 at about 2,252), and / and /tools/locallibrary dropped about 150 ms too. The patch adds the root error page's imports to the modulepreloads. Drop it once a SvelteKit release has the fix (sveltejs/kit#17416). The build test now counts the root error page in what every page loads. --- apps/web/tests/build.test.ts | 9 +++++---- patches/@sveltejs__kit@3.1.0.patch | 15 +++++++++++++++ pnpm-lock.yaml | 11 ++++++----- pnpm-workspace.yaml | 5 +++++ 4 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 patches/@sveltejs__kit@3.1.0.patch diff --git a/apps/web/tests/build.test.ts b/apps/web/tests/build.test.ts index 8296a208..38bec7ff 100644 --- a/apps/web/tests/build.test.ts +++ b/apps/web/tests/build.test.ts @@ -60,8 +60,9 @@ beforeAll(async () => { return [...keys].map((each) => chunks[each].file); }; - // Node 0 is the root layout, which every page renders. - initialLoad = new Set([...manifest.client.imports, ...nodeScripts(0)]); + // Node 0 is the root layout, which every page renders, and node 1 the root error page, which the + // client loads on every page before it hydrates. + initialLoad = new Set([...manifest.client.imports, ...nodeScripts(0), ...nodeScripts(1)]); routes = manifest.routes.flatMap(({ id, page }) => { if (!page) return []; @@ -92,8 +93,8 @@ it('ships no sourcemaps to the browser', () => { // that needs more than that is worth deciding on, not finding here later. it('keeps what every page loads within its budget', () => { - // The entry and the root layout with everything they import: SvelteKit's and Svelte's runtimes, - // devalue, and the small components pages share (vite.config.ts). About 172 KiB. + // The entry, the root layout and the root error page with everything they import: SvelteKit's and + // Svelte's runtimes, devalue, and the small components pages share (vite.config.ts). About 179 KiB. expect(size(initialLoad)).toBeLessThan(180 * 1024); }); diff --git a/patches/@sveltejs__kit@3.1.0.patch b/patches/@sveltejs__kit@3.1.0.patch new file mode 100644 index 00000000..f3c3e967 --- /dev/null +++ b/patches/@sveltejs__kit@3.1.0.patch @@ -0,0 +1,15 @@ +diff --git a/src/runtime/server/page/render.js b/src/runtime/server/page/render.js +index 0a8042eb03fa01a378517d8b5ef48891438122be..6106bfd9b62b535f1de568ab913302c530f6d1d6 100644 +--- a/src/runtime/server/page/render.js ++++ b/src/runtime/server/page/render.js +@@ -367,6 +367,10 @@ export async function render_response({ + } + + if (!client.inline) { ++ // The client awaits the root error page before it hydrates (`start` in client.js), so preload ++ // its JS too, or the browser only finds it after start.js has run (sveltejs/kit#17415). ++ for (const url of (await manifest.nodes[1]()).imports) modulepreloads.add(url); ++ + for (const dep of modulepreloads) { + const path = prefixed(dep); + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f3cc859b..e843c0a7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -209,6 +209,7 @@ overrides: patchedDependencies: '@stryker-mutator/vitest-runner@10.0.0': 7fafb35818cd76543d0c005132fe2b8e6fed3c52a39180616659246431acafeb + '@sveltejs/kit@3.1.0': e32df3bc288f7bc48be171fc52b26a8bea24b29db1e67b558add26930fa6a634 importers: @@ -434,10 +435,10 @@ importers: version: 10.6.2(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))(storybook@10.6.2(@types/react@19.3.0)(prettier@3.9.9)(react@19.3.0))(svelte@5.57.2)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) '@sveltejs/adapter-node': specifier: ^6.0.0 - version: 6.0.0(@sveltejs/kit@3.1.0(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))(svelte@5.57.2)(typescript@6.0.3)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))) + version: 6.0.0(@sveltejs/kit@3.1.0(patch_hash=e32df3bc288f7bc48be171fc52b26a8bea24b29db1e67b558add26930fa6a634)(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))(svelte@5.57.2)(typescript@6.0.3)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))) '@sveltejs/kit': specifier: ^3.0.0 - version: 3.1.0(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))(svelte@5.57.2)(typescript@6.0.3)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) + version: 3.1.0(patch_hash=e32df3bc288f7bc48be171fc52b26a8bea24b29db1e67b558add26930fa6a634)(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))(svelte@5.57.2)(typescript@6.0.3)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) '@sveltejs/vite-plugin-svelte': specifier: ^7.2.0 version: 7.3.1(svelte@5.57.2)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) @@ -10262,11 +10263,11 @@ snapshots: dependencies: acorn: 8.18.0 - '@sveltejs/adapter-node@6.0.0(@sveltejs/kit@3.1.0(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))(svelte@5.57.2)(typescript@6.0.3)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))': + '@sveltejs/adapter-node@6.0.0(@sveltejs/kit@3.1.0(patch_hash=e32df3bc288f7bc48be171fc52b26a8bea24b29db1e67b558add26930fa6a634)(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))(svelte@5.57.2)(typescript@6.0.3)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))': dependencies: - '@sveltejs/kit': 3.1.0(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))(svelte@5.57.2)(typescript@6.0.3)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) + '@sveltejs/kit': 3.1.0(patch_hash=e32df3bc288f7bc48be171fc52b26a8bea24b29db1e67b558add26930fa6a634)(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))(svelte@5.57.2)(typescript@6.0.3)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)) - '@sveltejs/kit@3.1.0(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))(svelte@5.57.2)(typescript@6.0.3)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))': + '@sveltejs/kit@3.1.0(patch_hash=e32df3bc288f7bc48be171fc52b26a8bea24b29db1e67b558add26930fa6a634)(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@7.3.1(svelte@5.57.2)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1)))(svelte@5.57.2)(typescript@6.0.3)(vite@8.3.4(@types/node@24.19.2)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)(yaml@2.9.1))': dependencies: '@rolldown/pluginutils': 1.0.1 '@standard-schema/spec': 1.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 812bd642..e108ce21 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -69,6 +69,11 @@ overrides: # release has the fix (#6214). patchedDependencies: '@stryker-mutator/vitest-runner@10.0.0': patches/@stryker-mutator__vitest-runner@10.0.0.patch + # SvelteKit's client imports the root error page before it hydrates, but the server doesn't + # preload it, so every page fetched it a round trip late (sveltejs/kit#17415). That round trip put + # /projects/halflight's simulated LCP over Lighthouse's budget on a busy machine. Drop the patch + # once a release has the fix (sveltejs/kit#17416). + '@sveltejs/kit@3.1.0': patches/@sveltejs__kit@3.1.0.patch # zod 3.x reaches prod only through @atproto/* (^3.23.8), and no 3.x release has the fix (4.6.6 # does). Drop this once @atproto moves to zod 4. auditConfig: