From 5c261239f0911a142b05b26ce81f18a7a0d2072c Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 10 Jul 2026 22:36:24 +0200 Subject: [PATCH 1/5] fix: preserve shared client chunk hashes when the app version changes The global `__sveltekit__abc123` is removed from the JS code in the client runtime, it now only exists in the on-the-fly-generated rendered HTML for app initialization and streaming chunks. That way the SvelteKit runtime stays stable between redeploys as long as it doesn't change, resulting in more cache hits. It's achieve by creating a new payload file, which the global value is passed to, and the other files then use the exported variable to get it. Because some eagerly require it we have to split up the dynamic imports. This should not matter because preloading loads and parses them in parallel anyway. Fixes #12260 --- .changeset/stable-version-build-hashes.md | 5 +++ packages/kit/src/exports/vite/index.js | 31 ++++++++++++------- packages/kit/src/runtime/app/env/internal.js | 5 ++- .../src/runtime/app/paths/internal/client.js | 6 ++-- .../kit/src/runtime/client/client-entry.js | 3 ++ packages/kit/src/runtime/client/client.js | 5 +-- packages/kit/src/runtime/client/entry.js | 25 +++++++++++++-- packages/kit/src/runtime/client/payload.js | 12 +++++++ .../kit/src/runtime/server/page/render.js | 13 ++++---- packages/kit/src/types/global-private.d.ts | 15 --------- packages/kit/src/types/internal.d.ts | 18 +++++++++++ 11 files changed, 98 insertions(+), 40 deletions(-) create mode 100644 .changeset/stable-version-build-hashes.md create mode 100644 packages/kit/src/runtime/client/client-entry.js create mode 100644 packages/kit/src/runtime/client/payload.js diff --git a/.changeset/stable-version-build-hashes.md b/.changeset/stable-version-build-hashes.md new file mode 100644 index 000000000000..b961c466d5d9 --- /dev/null +++ b/.changeset/stable-version-build-hashes.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +fix: preserve shared client chunk hashes when the app version changes diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 0e000666ae71..5d4dcfd54e7a 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -489,7 +489,6 @@ function kit({ svelte_config }) { new_config.define = { ...define, __SVELTEKIT_APP_VERSION_POLL_INTERVAL__: '0', - __SVELTEKIT_PAYLOAD__: kit_global, __SVELTEKIT_HAS_SERVER_LOAD__: 'true', __SVELTEKIT_HAS_UNIVERSAL_LOAD__: 'true' }; @@ -625,7 +624,7 @@ function kit({ svelte_config }) { return create_sveltekit_env_public( explicit_env_config, env, - `const env = ${kit_global}.env;` + `import { payload } from ${s(`${runtime_directory}/client/payload.js`)};\nconst env = payload.env;` ); case sveltekit_env_public_server: @@ -1301,6 +1300,7 @@ function kit({ svelte_config }) { client_input['bundle'] = `${runtime_directory}/client/bundle.js`; } else { client_input['entry/start'] = `${runtime_directory}/client/entry.js`; + client_input['entry/payload'] = `${runtime_directory}/client/payload.js`; client_input['entry/app'] = `${out_dir}/generated/client-optimized/app.js`; manifest_data.nodes.forEach((node, i) => { if (node.component || node.universal) { @@ -1380,8 +1380,7 @@ function kit({ svelte_config }) { // these are stubs that will be replaced after the initial server build define: { __SVELTEKIT_HAS_SERVER_LOAD__: 'true', - __SVELTEKIT_HAS_UNIVERSAL_LOAD__: 'true', - __SVELTEKIT_PAYLOAD__: '{}' + __SVELTEKIT_HAS_UNIVERSAL_LOAD__: 'true' } }, client: { @@ -1407,9 +1406,6 @@ function kit({ svelte_config }) { } : undefined } - }, - define: { - __SVELTEKIT_PAYLOAD__: kit_global } } }, @@ -1667,15 +1663,28 @@ function kit({ svelte_config }) { ); if (svelte_config.kit.output.bundleStrategy === 'split') { - const start = deps_of(`${runtime_directory}/client/entry.js`); + const start_entry = posixify(path.relative(root, `${runtime_directory}/client/entry.js`)); + const start = find_deps(vite_manifest, start_entry, false, root); + const runtime_entry = resolve_symlinks(vite_manifest, start_entry, root).chunk + .dynamicImports?.[0]; // client/entry.js dynamically imports client/client-entry.js + if (!runtime_entry) throw new Error('Could not find the client runtime chunk'); + const runtime = find_deps(vite_manifest, runtime_entry, false, root); const app = deps_of(`${out_dir}/generated/client-optimized/app.js`); build_data.client = { start: start.file, app: app.file, - imports: [...start.imports, ...app.imports], - stylesheets: [...start.stylesheets, ...app.stylesheets], - fonts: [...start.fonts, ...app.fonts], + imports: Array.from( + new Set([ + ...start.imports, + runtime.file, + ...runtime.imports, + app.file, + ...app.imports + ]) + ), + stylesheets: [...start.stylesheets, ...runtime.stylesheets, ...app.stylesheets], + fonts: [...start.fonts, ...runtime.fonts, ...app.fonts], uses_env_dynamic_public }; diff --git a/packages/kit/src/runtime/app/env/internal.js b/packages/kit/src/runtime/app/env/internal.js index 1d438b38e870..87b3ca78ed51 100644 --- a/packages/kit/src/runtime/app/env/internal.js +++ b/packages/kit/src/runtime/app/env/internal.js @@ -1,4 +1,7 @@ -export const version = __SVELTEKIT_APP_VERSION__; +import { BROWSER } from 'esm-env'; +import { payload } from '../../client/payload.js'; + +export const version = BROWSER ? payload.version : __SVELTEKIT_APP_VERSION__; export let building = false; export let prerendering = false; diff --git a/packages/kit/src/runtime/app/paths/internal/client.js b/packages/kit/src/runtime/app/paths/internal/client.js index a5679afff6ef..a35036645816 100644 --- a/packages/kit/src/runtime/app/paths/internal/client.js +++ b/packages/kit/src/runtime/app/paths/internal/client.js @@ -1,4 +1,6 @@ -export const base = __SVELTEKIT_PAYLOAD__?.base ?? __SVELTEKIT_PATHS_BASE__; -export const assets = __SVELTEKIT_PAYLOAD__?.assets ?? base ?? __SVELTEKIT_PATHS_ASSETS__; +import { payload } from '../../../client/payload.js'; + +export const base = payload.base ?? __SVELTEKIT_PATHS_BASE__; +export const assets = payload.assets ?? base ?? __SVELTEKIT_PATHS_ASSETS__; export const app_dir = __SVELTEKIT_APP_DIR__; export const hash_routing = __SVELTEKIT_HASH_ROUTING__; diff --git a/packages/kit/src/runtime/client/client-entry.js b/packages/kit/src/runtime/client/client-entry.js new file mode 100644 index 000000000000..74b0fb283a58 --- /dev/null +++ b/packages/kit/src/runtime/client/client-entry.js @@ -0,0 +1,3 @@ +// we expose this as a separate entry point (rather than treating client.js as the entry point) +// so that everything other than `start`/`load_css` can be treeshaken +export { start, load_css } from './client.js'; diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 3cb7b4a53f45..0d1985285b13 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -43,6 +43,7 @@ import { } from '../shared.js'; import { get_message, get_status } from '../../utils/error.js'; import { page, update, navigating, updated } from './state.svelte.js'; +import { payload } from './payload.js'; import { add_data_suffix, add_resolution_suffix } from '../pathname.js'; import { noop_span } from '../telemetry/noop.js'; import { read_ndjson } from './ndjson.js'; @@ -323,8 +324,8 @@ export async function start(_app, _target, hydrate) { ); } - if (__SVELTEKIT_PAYLOAD__.data) { - const { q = {}, p = {}, l = {}, f = {} } = __SVELTEKIT_PAYLOAD__.data; + if (payload.data) { + const { q = {}, p = {}, l = {}, f = {} } = payload.data; // store the whole nodes — error records seed the corresponding // resources in a failed state when they are created during hydration diff --git a/packages/kit/src/runtime/client/entry.js b/packages/kit/src/runtime/client/entry.js index 74b0fb283a58..0bfd62bc0a81 100644 --- a/packages/kit/src/runtime/client/entry.js +++ b/packages/kit/src/runtime/client/entry.js @@ -1,3 +1,22 @@ -// we expose this as a separate entry point (rather than treating client.js as the entry point) -// so that everything other than `start`/`load_css` can be treeshaken -export { start, load_css } from './client.js'; +import { set_payload } from './payload.js'; + +/** @type {Promise} */ +let client; + +/** @param {import('types').SvelteKitPayload} payload */ +export function init(payload) { + set_payload(payload); + // Importing the client after setting the payload ensures that modules such as + // `$app/paths` can read it during initialization. + client ??= import('./client-entry.js'); +} + +/** @param {Parameters} args */ +export async function start(...args) { + return (await client).start(...args); +} + +/** @param {Parameters} args */ +export async function load_css(...args) { + return (await client).load_css(...args); +} diff --git a/packages/kit/src/runtime/client/payload.js b/packages/kit/src/runtime/client/payload.js new file mode 100644 index 000000000000..d15e6f98f521 --- /dev/null +++ b/packages/kit/src/runtime/client/payload.js @@ -0,0 +1,12 @@ +/** @import {SvelteKitPayload} from 'types'; */ + +/** + * Code inside the SvelteKit client runtime should only use this, not the global, + * so that the file hashes stay stable between rebuilds as long as the SvelteKit runtime doesn't change + */ +export let payload = /** @type {SvelteKitPayload} */ ({}); + +/** @param {SvelteKitPayload} value */ +export function set_payload(value) { + payload = value; +} diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index 583d4584324f..5c2dca9989ba 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -412,7 +412,7 @@ export async function render_response({ const blocks = []; - const properties = [`base: ${base_expression}`]; + const properties = [`base: ${base_expression}`, `version: ${s(__SVELTEKIT_APP_VERSION__)}`]; if (paths.assets) { properties.push(`assets: ${s(paths.assets)}`); @@ -435,7 +435,9 @@ export async function render_response({ if (client.inline) { app_declaration = `const app = ${global}.app.app;`; } else if (client.app) { - app_declaration = `const app = await import(${s(prefixed(client.app))});`; + app_declaration = `const kit = await import(${s(prefixed(client.start))}); + kit.init(${global}); + const app = await import(${s(prefixed(client.app))});`; } else { app_declaration = `const { app } = await import(${s(prefixed(client.start))});`; } @@ -529,10 +531,9 @@ export async function render_response({ ${serialized_data}${global}.app.start(${args.join(', ')});` : client.app - ? `Promise.all([ - import(${s(prefixed(client.start))}), - import(${s(prefixed(client.app))}) - ]).then(([kit, app]) => { + ? `import(${s(prefixed(client.start))}).then(async (kit) => { + kit.init(${global}); + const app = await import(${s(prefixed(client.app))}); ${serialized_data}kit.start(app, ${args.join(', ')}); });` : `import(${s(prefixed(client.start))}).then((app) => { diff --git a/packages/kit/src/types/global-private.d.ts b/packages/kit/src/types/global-private.d.ts index 7c7bed787169..365d0fdc6565 100644 --- a/packages/kit/src/types/global-private.d.ts +++ b/packages/kit/src/types/global-private.d.ts @@ -33,21 +33,6 @@ declare global { * Used for treeshaking universal load code from client bundles when no universal loads exist. */ const __SVELTEKIT_HAS_UNIVERSAL_LOAD__: boolean; - /** The `__sveltekit_abc123` object in the init `