From 92c3c78d3148c77a3fa2d5860a3781106e6ee000 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 20 Oct 2025 07:58:26 +0200 Subject: [PATCH 1/4] chore: treeshake load function code if we know it's unused This shaves off about ~1kb minified when you're not using any load functions in your code base, which can become more common with load functions --- .changeset/eleven-taxis-deny.md | 5 + packages/kit/src/core/postbuild/analyse.js | 3 +- packages/kit/src/exports/vite/index.js | 27 +++-- packages/kit/src/runtime/client/client.js | 117 +++++++++++---------- packages/kit/src/types/global-private.d.ts | 10 ++ packages/kit/src/types/internal.d.ts | 1 + 6 files changed, 100 insertions(+), 63 deletions(-) create mode 100644 .changeset/eleven-taxis-deny.md diff --git a/.changeset/eleven-taxis-deny.md b/.changeset/eleven-taxis-deny.md new file mode 100644 index 000000000000..676f1efb1154 --- /dev/null +++ b/.changeset/eleven-taxis-deny.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': patch +--- + +chore: treeshake load function code if we know it's unused diff --git a/packages/kit/src/core/postbuild/analyse.js b/packages/kit/src/core/postbuild/analyse.js index c71a8addf65a..997c8fc957fd 100644 --- a/packages/kit/src/core/postbuild/analyse.js +++ b/packages/kit/src/core/postbuild/analyse.js @@ -105,7 +105,8 @@ async function analyse({ } metadata.nodes[node.index] = { - has_server_load: has_server_load(node) + has_server_load: has_server_load(node), + has_universal_load: node.universal?.load !== undefined }; } diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index 0241d226069f..e7c8968afa8f 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -172,8 +172,8 @@ let secondary_build_started = false; /** @type {import('types').ManifestData} */ let manifest_data; -/** @type {import('types').ServerMetadata['remotes'] | undefined} only set at build time */ -let remote_exports = undefined; +/** @type {import('types').ServerMetadata | undefined} only set at build time once analysis is finished */ +let build_metadata = undefined; /** * Returns the SvelteKit Vite plugin. Vite executes Rollup hooks as well as some of its own. @@ -369,12 +369,27 @@ async function kit({ svelte_config }) { if (!secondary_build_started) { manifest_data = sync.all(svelte_config, config_env.mode).manifest_data; + // During the initial server build we don't know yet + new_config.define.__SVELTEKIT_HAS_SERVER_LOAD__ = 'true'; + new_config.define.__SVELTEKIT_HAS_UNIVERSAL_LOAD__ = 'true'; + } else { + // Through the finished analysis we can now check if any node has server or universal load functions + const has_server_load = build_metadata + ? Object.values(build_metadata.nodes).some((node) => node.has_server_load) + : true; + const has_universal_load = build_metadata + ? Object.values(build_metadata.nodes).some((node) => node.has_universal_load) + : true; + new_config.define.__SVELTEKIT_HAS_SERVER_LOAD__ = s(has_server_load); + new_config.define.__SVELTEKIT_HAS_UNIVERSAL_LOAD__ = s(has_universal_load); } } else { new_config.define = { ...define, __SVELTEKIT_APP_VERSION_POLL_INTERVAL__: '0', - __SVELTEKIT_PAYLOAD__: 'globalThis.__sveltekit_dev' + __SVELTEKIT_PAYLOAD__: 'globalThis.__sveltekit_dev', + __SVELTEKIT_HAS_SERVER_LOAD__: 'true', + __SVELTEKIT_HAS_UNIVERSAL_LOAD__: 'true' }; // @ts-ignore this prevents a reference error if `client.js` is imported on the server @@ -733,8 +748,8 @@ async function kit({ svelte_config }) { // in prod, we already built and analysed the server code before // building the client code, so `remote_exports` is populated - else if (remote_exports) { - const exports = remote_exports.get(remote.hash); + else if (build_metadata?.remotes) { + const exports = build_metadata?.remotes.get(remote.hash); if (!exports) throw new Error('Expected to find metadata for remote file ' + id); for (const [name, value] of exports) { @@ -1038,7 +1053,7 @@ async function kit({ svelte_config }) { remotes }); - remote_exports = metadata.remotes; + build_metadata = metadata; log.info('Building app'); diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 4e79a1300d16..768ee347ee4c 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -732,7 +732,7 @@ async function load_node({ loader, parent, url, params, route, server_data_node } } - if (node.universal?.load) { + if (__SVELTEKIT_HAS_UNIVERSAL_LOAD__ && node.universal?.load) { /** @param {string[]} deps */ function depends(...deps) { for (const dep of deps) { @@ -1004,49 +1004,52 @@ async function load_route({ id, invalidating, url, params, route, preload }) { const search_params_changed = diff_search_params(current.url, url); let parent_invalid = false; - const invalid_server_nodes = loaders.map((loader, i) => { - const previous = current.branch[i]; - const invalid = - !!loader?.[0] && - (previous?.loader !== loader[1] || - has_changed( - parent_invalid, - route_changed, - url_changed, - search_params_changed, - previous.server?.uses, - params - )); - - if (invalid) { - // For the next one - parent_invalid = true; - } + if (__SVELTEKIT_HAS_SERVER_LOAD__) { + const invalid_server_nodes = loaders.map((loader, i) => { + const previous = current.branch[i]; + + const invalid = + !!loader?.[0] && + (previous?.loader !== loader[1] || + has_changed( + parent_invalid, + route_changed, + url_changed, + search_params_changed, + previous.server?.uses, + params + )); + + if (invalid) { + // For the next one + parent_invalid = true; + } - return invalid; - }); + return invalid; + }); - if (invalid_server_nodes.some(Boolean)) { - try { - server_data = await load_data(url, invalid_server_nodes); - } catch (error) { - const handled_error = await handle_error(error, { url, params, route: { id } }); + if (invalid_server_nodes.some(Boolean)) { + try { + server_data = await load_data(url, invalid_server_nodes); + } catch (error) { + const handled_error = await handle_error(error, { url, params, route: { id } }); - if (preload_tokens.has(preload)) { - return preload_error({ error: handled_error, url, params, route }); - } + if (preload_tokens.has(preload)) { + return preload_error({ error: handled_error, url, params, route }); + } - return load_root_error_page({ - status: get_status(error), - error: handled_error, - url, - route - }); - } + return load_root_error_page({ + status: get_status(error), + error: handled_error, + url, + route + }); + } - if (server_data.type === 'redirect') { - return server_data; + if (server_data.type === 'redirect') { + return server_data; + } } } @@ -1232,27 +1235,29 @@ async function load_root_error_page({ status, error, url, route }) { /** @type {import('types').ServerDataNode | null} */ let server_data_node = null; - const default_layout_has_server_load = app.server_loads[0] === 0; + if (__SVELTEKIT_HAS_SERVER_LOAD__) { + const default_layout_has_server_load = app.server_loads[0] === 0; - if (default_layout_has_server_load) { - // TODO post-https://github.com/sveltejs/kit/discussions/6124 we can use - // existing root layout data - try { - const server_data = await load_data(url, [true]); + if (default_layout_has_server_load) { + // TODO post-https://github.com/sveltejs/kit/discussions/6124 we can use + // existing root layout data + try { + const server_data = await load_data(url, [true]); - if ( - server_data.type !== 'data' || - (server_data.nodes[0] && server_data.nodes[0].type !== 'data') - ) { - throw 0; - } + if ( + server_data.type !== 'data' || + (server_data.nodes[0] && server_data.nodes[0].type !== 'data') + ) { + throw 0; + } - server_data_node = server_data.nodes[0] ?? null; - } catch { - // at this point we have no choice but to fall back to the server, if it wouldn't - // bring us right back here, turning this into an endless loop - if (url.origin !== origin || url.pathname !== location.pathname || hydrated) { - await native_navigation(url); + server_data_node = server_data.nodes[0] ?? null; + } catch { + // at this point we have no choice but to fall back to the server, if it wouldn't + // bring us right back here, turning this into an endless loop + if (url.origin !== origin || url.pathname !== location.pathname || hydrated) { + await native_navigation(url); + } } } } diff --git a/packages/kit/src/types/global-private.d.ts b/packages/kit/src/types/global-private.d.ts index beaba7ec87d7..3ab871bd5b48 100644 --- a/packages/kit/src/types/global-private.d.ts +++ b/packages/kit/src/types/global-private.d.ts @@ -13,6 +13,16 @@ declare global { const __SVELTEKIT_EXPERIMENTAL__REMOTE_FUNCTIONS__: boolean; /** True if `config.kit.router.resolution === 'client'` */ const __SVELTEKIT_CLIENT_ROUTING__: boolean; + /** + * True if any node in the manifest has a server load function. + * Used for treeshaking server load code from client bundles when no server loads exist. + */ + const __SVELTEKIT_HAS_SERVER_LOAD__: boolean; + /** + * True if any node in the manifest has a universal load function. + * 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 `