From 3723b425a73bae1c8c240cf3320563b89ca429ed Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 20 Jul 2026 16:41:59 -0400 Subject: [PATCH 1/2] chore: DRY out preload link/header logic --- .../kit/src/runtime/server/page/render.js | 47 ++++++++----------- 1 file changed, 20 insertions(+), 27 deletions(-) diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index c63329967881..c7839b014863 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -304,6 +304,19 @@ export async function render_response({ head.add_style(style, attributes); } + /** + * see the `output.linkHeaderPreload` option for details on why we have multiple options here + * @param {string} path + * @param {string[]} attributes + */ + const add_preload = (path, attributes) => { + if (options.link_header_preload && !state.prerendering) { + link_headers.add(`<${encodeURI(path)}>; ${attributes.join('; ')}; nopush`); + } else { + head.add_link_tag(path, attributes); + } + }; + for (const dep of stylesheets) { const path = prefixed(dep); @@ -328,17 +341,8 @@ export async function render_response({ if (resolve_opts.preload({ type: 'font', path })) { const ext = dep.slice(dep.lastIndexOf('.') + 1); - if (options.link_header_preload && !state.prerendering) { - link_headers.add( - `<${encodeURI(path)}>; rel="preload"; as="font"; type="font/${ext}"; crossorigin; nopush` - ); - } else { - head.add_link_tag(path, [ - 'rel="preload"', - 'as="font"', - `type="font/${ext}"`, - 'crossorigin' - ]); + if (resolve_opts.preload({ type: 'font', path })) { + add_preload(path, ['rel="preload"', 'as="font"', `type="font/${ext}"`, 'crossorigin']); } } } @@ -368,23 +372,12 @@ export async function render_response({ } if (!client.inline) { - const included_modulepreloads = Array.from(modulepreloads, (dep) => prefixed(dep)).filter( - (path) => resolve_opts.preload({ type: 'js', path }) - ); - - /** @type {(path: string) => void} */ - let add_preload; + for (const dep of modulepreloads) { + const path = prefixed(dep); - // see the output.preloadStrategy option for details on why we have multiple options here - if (options.link_header_preload && !state.prerendering) { - add_preload = (path) => - link_headers.add(`<${encodeURI(path)}>; rel="modulepreload"; nopush`); - } else { - add_preload = (path) => head.add_link_tag(path, ['rel="modulepreload"']); - } - - for (const path of included_modulepreloads) { - add_preload(path); + if (resolve_opts.preload({ type: 'js', path })) { + add_preload(path, ['rel="modulepreload"']); + } } } From a9ae4af91b4ca70cb19d82b4b848c5e9619042d6 Mon Sep 17 00:00:00 2001 From: "vercel[bot]" <35613825+vercel[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2026 20:53:54 +0000 Subject: [PATCH 2/2] Fix: The user-provided `resolve_opts.preload` hook is invoked twice per font due to a redundant nested `if` check in the fonts loop of `render_response`. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit fixes the issue reported at packages/kit/src/runtime/server/page/render.js:344 ## Bug In the fonts loop of `render_response` (`packages/kit/src/runtime/server/page/render.js`), the code contained a redundant nested check: ```js if (resolve_opts.preload({ type: 'font', path })) { const ext = dep.slice(dep.lastIndexOf('.') + 1); if (resolve_opts.preload({ type: 'font', path })) { add_preload(path, ['rel="preload"', 'as="font"', `type="font/${ext}"`, 'crossorigin']); } } ``` `resolve_opts.preload` is a **user-provided hook** (via the `preload` option of `resolve` in `hooks.server.js`). It is not guaranteed to be pure or idempotent — a user hook may increment counters, log, or track which assets were preloaded. Calling it twice per font is unintended behavior. ### Trigger Any request rendering a page/route that includes font dependencies, when the app defines a `resolve(..., { preload })` hook. The hook is executed a second time for every font whose first `preload` call returned truthy. A hook with side effects (e.g. counting preloaded fonts) would observe double invocation. ## Fix Removed the redundant inner `if`, calling `add_preload` directly after the single outer `resolve_opts.preload` check. This matches the pattern used for the modulepreloads loop, which calls `add_preload` directly after one hook check, ensuring the hook is invoked exactly once per font. Co-authored-by: Vercel Co-authored-by: Rich-Harris --- packages/kit/src/runtime/server/page/render.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js index c7839b014863..676afaf8f56d 100644 --- a/packages/kit/src/runtime/server/page/render.js +++ b/packages/kit/src/runtime/server/page/render.js @@ -341,9 +341,7 @@ export async function render_response({ if (resolve_opts.preload({ type: 'font', path })) { const ext = dep.slice(dep.lastIndexOf('.') + 1); - if (resolve_opts.preload({ type: 'font', path })) { - add_preload(path, ['rel="preload"', 'as="font"', `type="font/${ext}"`, 'crossorigin']); - } + add_preload(path, ['rel="preload"', 'as="font"', `type="font/${ext}"`, 'crossorigin']); } }