Skip to content

fix(html): don't inline preload link targets (fix #13355) - #23387

Open
kakiuwang-ui wants to merge 3 commits into
vitejs:mainfrom
kakiuwang-ui:fix/html-no-inline-modulepreload
Open

fix(html): don't inline preload link targets (fix #13355)#23387
kakiuwang-ui wants to merge 3 commits into
vitejs:mainfrom
kakiuwang-ui:fix/html-no-inline-modulepreload

Conversation

@kakiuwang-ui

@kakiuwang-ui kakiuwang-ui commented Aug 27, 2026

Copy link
Copy Markdown

Description

A <link rel="modulepreload"> whose target is smaller than build.assetsInlineLimit gets rewritten to a data: URL at build time:

<!-- source -->
<link rel="modulepreload" href="/worker.js" />

<!-- built -->
<link rel="modulepreload" href="data:text/javascript;base64,Ly8gcmVmZXJl..." />

That data URL is a different module specifier from the emitted chunk, so the browser warms a module nothing else on the page will request. The preload becomes a silent no-op.

Cause

In html.ts, a link[href] that isn't a CSS request falls through to processAssetUrl(url, shouldInline) with shouldInline === undefined, which leaves the decision to the default size heuristic. There is already a noInlineLinkRels set for rels where inlining is wrong (icon, manifest, ...), and modulepreload belongs in it for the same reason.

Fix

Add modulepreload to noInlineLinkRels.

Scope

Covers modulepreload, preload and prefetch (preload/prefetch added on review request).

For preload this needed one extra thing: <link rel="preload" as="image" imagesrcset> is handled by the srcset branch, which called processAssetUrl without a shouldInline argument and so ignored noInlineLinkRels. The no-inline decision is now hoisted out of the src branch so both use it.

<link rel="preload" as="style" href="x.css"> is untouched — the isCSSRequest check above converts it into a CSS import before reaching this code.

#13355 also mentions that the emitted file is the unminified original, i.e. it never went through the JS pipeline at all. That's a distinct problem (related: #9952) and isn't addressed here.

Tests

Added links to the assets playground covering all four cases — modulepreload href, preload href, preload imagesrcset, prefetch href — pointing at files well under that playground's assetsInlineLimit: 8000, so they are inlined without the fix.

Each fails on main with the reported symptom, e.g.:

AssertionError: expected 'data:text/javascript;base64,Ly8gcmVmZ…' to match /\/foo\/bar\/assets\/preload-module-…/
AssertionError: expected 'data:image/png;base64,iVBORw0KGgoAAAA…' to match /\/foo\/bar\/assets\/preload-asset-…/

and passes with the fix, in both serve and build mode.

fix #13355

A `<link rel="modulepreload">` whose target is smaller than
`build.assetsInlineLimit` was rewritten to a `data:` URL. The browser
fetches that data URL as a module distinct from the emitted chunk, so
nothing referenced by the page is warmed and the preload is a no-op.

Add `modulepreload` to `noInlineLinkRels` so these targets are always
emitted as files, matching the existing handling for `icon` and
`manifest` links.

`preload`/`prefetch` links have the same problem but are left alone
here; they interact with vitejs#16269 and need to be handled separately.
@github-actions github-actions Bot added the bot: maybe Maybe a bot, LLM, or agent label Aug 27, 2026
Comment thread packages/vite/src/node/plugins/html.ts Outdated
bluwy
bluwy previously approved these changes Sep 1, 2026
@bluwy

bluwy commented Sep 1, 2026

Copy link
Copy Markdown
Member

preload and prefetch are broken the same way, but they overlap with #16269, so I left them out to keep this reviewable — happy to follow up separately if you'd prefer them handled together.

I think we can fix these together. Can you add them in this PR?

Extend the previous commit to `rel="preload"` and `rel="prefetch"`,
which are defeated by inlining for the same reason as `modulepreload`.

`<link rel="preload" as="image" imagesrcset>` goes through the srcset
branch, which called `processAssetUrl` without a `shouldInline`
argument and so ignored `noInlineLinkRels` entirely. Hoist the
no-inline decision out of the `src` branch so both use it.

Also drop the comment above `noInlineLinkRels` per review.
@kakiuwang-ui

Copy link
Copy Markdown
Author

Done — preload and prefetch are in now, and thanks for applying the comment suggestion.

While adding them I found the fix wasn't complete for preload: <link rel="preload" as="image" imagesrcset> is handled by the srcset branch, which called processAssetUrl(url) with no shouldInline argument, so it never consulted noInlineLinkRels at all. Confirmed against a build before the change:

<link class="preload-imagesrcset" rel="preload" as="image"
      imagesrcset="data:image/png;base64,iVBORw0KGgoAAAAN..." />

So I hoisted the no-inline decision out of the src branch to the top of the for (const attr of assetAttributes) loop and passed it to both. Happy to drop that part if you'd rather keep this PR to href only.

Tests now cover href for preload/prefetch plus the imagesrcset path; all three are inlined without the change and emitted as files with it.

One thing I did not touch: <link rel="preload" as="style" href="x.css"> never reaches this code, because the isCSSRequest(url) check above it converts the link into a CSS import first. That looked like pre-existing behaviour rather than something to change here.

@kakiuwang-ui kakiuwang-ui changed the title fix(html): don't inline modulepreload link targets (fix #13355) fix(html): don't inline preload link targets (fix #13355) Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot: maybe Maybe a bot, LLM, or agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot preload workers

2 participants