diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts
index d3488d5bb092c9..2f3e005a782423 100644
--- a/packages/vite/src/node/plugins/html.ts
+++ b/packages/vite/src/node/plugins/html.ts
@@ -168,6 +168,9 @@ const noInlineLinkRels = new Set([
'apple-touch-icon',
'apple-touch-startup-image',
'manifest',
+ 'modulepreload',
+ 'preload',
+ 'prefetch',
])
export const isAsyncScriptMap: WeakMap<
@@ -622,6 +625,16 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
config.html?.additionalAssetSources,
)
for (const attr of assetAttributes) {
+ // If the node is a link, check if it can be inlined. If not, set `shouldInline`
+ // to `false` to force no inline. If `undefined`, it leaves to the default heuristics.
+ const isNoInlineLink =
+ node.nodeName === 'link' &&
+ attr.attributes.rel &&
+ parseRelAttr(attr.attributes.rel).some((v) =>
+ noInlineLinkRels.has(v),
+ )
+ const shouldInline = isNoInlineLink ? false : undefined
+
if (attr.type === 'remove') {
s.remove(attr.location.startOffset, attr.location.endOffset)
continue
@@ -636,7 +649,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
decodedUrl !== undefined &&
!isExcludedUrl(decodedUrl)
) {
- const result = await processAssetUrl(url)
+ const result = await processAssetUrl(url, shouldInline)
return result !== decodedUrl
? encodeURIPath(result)
: url
@@ -675,15 +688,6 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin {
})
js += importExpression
} else {
- // If the node is a link, check if it can be inlined. If not, set `shouldInline`
- // to `false` to force no inline. If `undefined`, it leaves to the default heuristics.
- const isNoInlineLink =
- node.nodeName === 'link' &&
- attr.attributes.rel &&
- parseRelAttr(attr.attributes.rel).some((v) =>
- noInlineLinkRels.has(v),
- )
- const shouldInline = isNoInlineLink ? false : undefined
assetUrlsPromises.push(
(async () => {
const processedUrl = await processAssetUrl(
diff --git a/playground/assets/__tests__/assets.spec.ts b/playground/assets/__tests__/assets.spec.ts
index 46e540d5c7a610..f568d4c3eeb5dd 100644
--- a/playground/assets/__tests__/assets.spec.ts
+++ b/playground/assets/__tests__/assets.spec.ts
@@ -314,6 +314,36 @@ describe('css url() references', () => {
expect(await getBg('.css-url-quotes-base64-inline')).toMatch(match)
})
+ test('no base64 inline for modulepreload links', async () => {
+ const el = await page.$(`link[rel="modulepreload"]`)
+ const href = await el.getAttribute('href')
+ expect(href).toMatch(
+ isBundled
+ ? /\/foo\/bar\/assets\/preload-module-[-\w]{8}\.js/
+ : 'preload-module.js',
+ )
+ })
+
+ test('no base64 inline for preload and prefetch links', async () => {
+ const preloadAssetMatch = isBundled
+ ? /\/foo\/bar\/assets\/preload-asset-[-\w]{8}\.png/
+ : '/foo/bar/nested/preload-asset.png'
+
+ const preloadEl = await page.$('link.preload-href')
+ expect(await preloadEl.getAttribute('href')).toMatch(preloadAssetMatch)
+
+ const prefetchEl = await page.$('link.prefetch-href')
+ expect(await prefetchEl.getAttribute('href')).toMatch(preloadAssetMatch)
+
+ // `imagesrcset` goes through the srcset branch, which has to honour the
+ // same no-inline decision as `href`
+ const imageSrcSetEl = await page.$('link.preload-imagesrcset')
+ const imageSrcSet = await imageSrcSetEl.getAttribute('imagesrcset')
+ imageSrcSet.split(', ').forEach((s) => {
+ expect(s).toMatch(preloadAssetMatch)
+ })
+ })
+
test('no base64 inline for icon and manifest links', async () => {
const iconEl = await page.$(`link.ico`)
const href = await iconEl.getAttribute('href')
diff --git a/playground/assets/index.html b/playground/assets/index.html
index 6ba5233498bcd4..728c3687e5b554 100644
--- a/playground/assets/index.html
+++ b/playground/assets/index.html
@@ -4,6 +4,24 @@
+
+
+
+
, small enough to hit assetsInlineLimit
+export const preloadedModule = 'preloaded'