diff --git a/docs/config/build-options.md b/docs/config/build-options.md index e4882e111c51ee..a311f5956615ba 100644 --- a/docs/config/build-options.md +++ b/docs/config/build-options.md @@ -48,10 +48,12 @@ Specify the directory to nest generated assets under (relative to `build.outDir` ## build.assetsInlineLimit -- **Type:** `number` -- **Default:** `4096` (4kb) +- **Type:** `number` | `((filePath: string, size: number, totalBundledSize: number) => boolean)` +- **Default:** `6144` (6kb) Imported or referenced assets that are smaller than this threshold will be inlined as base64 URLs to avoid extra http requests. Set to `0` to disable inlining altogether. +Can also implement a function that return boolean if it should bundled. +Passes the `filePath: string`, `contentSize: number` and currently accrued bundled size `totalBundledSize: number` ::: tip Note If you specify `build.lib`, `build.assetsInlineLimit` will be ignored and assets will always be inlined, regardless of file size. diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 294ff48654d301..f978724f3cbf32 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -87,10 +87,14 @@ export interface BuildOptions { assetsDir?: string /** * Static asset files smaller than this number (in bytes) will be inlined as - * base64 strings. Default limit is `4096` (4kb). Set to `0` to disable. + * base64 strings. Default limit is `6144` (6kb). Set to `0` to disable. + * Can also implement a function that return boolean if it should bundled. + * Passes the `filePath`, `contentSize` and currently accrued bundled size `totalBundledSize` * @default 4096 */ - assetsInlineLimit?: number + assetsInlineLimit?: + | number + | ((filePath: string, size: number, totalBundledSize: number) => boolean) /** * Whether to code-split CSS. When enabled, CSS in async chunks will be * inlined as strings in the chunk and inserted via dynamically created @@ -239,7 +243,7 @@ export function resolveBuildOptions( polyfillModulePreload: true, outDir: 'dist', assetsDir: 'assets', - assetsInlineLimit: 4096, + assetsInlineLimit: 6144, cssCodeSplit: !raw?.lib, cssTarget: false, sourcemap: false, diff --git a/packages/vite/src/node/cli.ts b/packages/vite/src/node/cli.ts index 86ae76bba12215..d8fceda388fe8c 100644 --- a/packages/vite/src/node/cli.ts +++ b/packages/vite/src/node/cli.ts @@ -137,7 +137,7 @@ cli ) .option( '--assetsInlineLimit ', - `[number] static asset base64 inline threshold in bytes (default: 4096)` + `[number] static asset base64 inline threshold in bytes (default: 6144)` ) .option( '--ssr [entry]', diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 0403009d4b69c4..b090379526df91 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -15,7 +15,10 @@ export const assetUrlRE = /__VITE_ASSET__([a-z\d]{8})__(?:\$_(.*?)__)?/g const rawRE = /(\?|&)raw(?:&|$)/ const urlRE = /(\?|&)url(?:&|$)/ -const assetCache = new WeakMap>() +const assetCache = new WeakMap< + ResolvedConfig, + Map +>() const assetHashToFilenameMap = new WeakMap< ResolvedConfig, @@ -364,6 +367,11 @@ export function publicFileToBuiltUrl( return `__VITE_PUBLIC_ASSET__${hash}__` } +const byteSizeOf = (function () { + const encoder = new TextEncoder() + const encode = encoder.encode.bind(encoder) + return (input: string) => encode(input).length +})() /** * Register an asset to be emitted as part of the bundle (if necessary) * and returns the resolved public URL @@ -381,63 +389,97 @@ async function fileToBuiltUrl( const cache = assetCache.get(config)! const cached = cache.get(id) if (cached) { - return cached + return cached.url } const file = cleanUrl(id) const content = await fsp.readFile(file) - let url: string - if ( - config.build.lib || - (!file.endsWith('.svg') && - !file.endsWith('.html') && - content.length < Number(config.build.assetsInlineLimit)) - ) { - const mimeType = mrmime.lookup(file) ?? 'application/octet-stream' - // base64 inlined as a string - url = `data:${mimeType};base64,${content.toString('base64')}` - } else { - // emit as asset - // rollup supports `import.meta.ROLLUP_FILE_URL_*`, but it generates code - // that uses runtime url sniffing and it can be verbose when targeting - // non-module format. It also fails to cascade the asset content change - // into the chunk's hash, so we have to do our own content hashing here. - // https://bundlers.tooling.report/hashing/asset-cascade/ - // https://github.com/rollup/rollup/issues/3415 - const map = assetHashToFilenameMap.get(config)! - const contentHash = getHash(content) - const { search, hash } = parseUrl(id) - const postfix = (search || '') + (hash || '') - - const fileName = assetFileNamesToFileName( - resolveAssetFileNames(config), - file, - contentHash, - content - ) - if (!map.has(contentHash)) { - map.set(contentHash, fileName) - } - const emittedSet = emittedHashMap.get(config)! - if (!emittedSet.has(contentHash)) { - const name = normalizePath(path.relative(config.root, file)) - pluginContext.emitFile({ - name, - fileName, - type: 'asset', - source: content - }) - emittedSet.add(contentHash) + let size: number + + /* + lib should always inlined + svg should never be inlined (unless lib) + */ + if (config.build.lib) { + url = fileToInlinedAsset(file, content) + size = 0 + } else if (file.endsWith('.svg') === false) { + const inlinedURL = fileToInlinedAsset(file, content) + const inlinedSize: number = byteSizeOf(inlinedURL) + + const assetInlineLimit = config.build.assetsInlineLimit ?? 0 + + const shouldInline = + typeof assetInlineLimit === 'number' + ? inlinedSize < Number(assetInlineLimit) + : assetInlineLimit( + file, + inlinedSize, + [...cache.values()].reduce((memo, { size }) => memo + size, 0) + ) + + if (shouldInline) { + size = inlinedSize + url = inlinedURL } - - url = `__VITE_ASSET__${contentHash}__${postfix ? `$_${postfix}__` : ``}` // TODO_BASE } - cache.set(id, url) + url ??= fileToLinkedAsset(id, config, pluginContext, file, content) + size ||= 0 + + cache.set(id, { url, size }) return url } +function fileToInlinedAsset(file: string, content: Buffer): string { + const mimeType = mrmime.lookup(file) ?? 'application/octet-stream' + return `data:${mimeType};base64,${content.toString('base64')}` +} + +function fileToLinkedAsset( + id: string, + config: ResolvedConfig, + pluginContext: PluginContext, + file: string, + content: Buffer +): string { + // emit as asset + // rollup supports `import.meta.ROLLUP_FILE_URL_*`, but it generates code + // that uses runtime url sniffing and it can be verbose when targeting + // non-module format. It also fails to cascade the asset content change + // into the chunk's hash, so we have to do our own content hashing here. + // https://bundlers.tooling.report/hashing/asset-cascade/ + // https://github.com/rollup/rollup/issues/3415 + const map = assetHashToFilenameMap.get(config)! + const contentHash = getHash(content) + const { search, hash } = parseUrl(id) + const postfix = (search || '') + (hash || '') + + const fileName = assetFileNamesToFileName( + resolveAssetFileNames(config), + file, + contentHash, + content + ) + if (!map.has(contentHash)) { + map.set(contentHash, fileName) + } + const emittedSet = emittedHashMap.get(config)! + if (!emittedSet.has(contentHash)) { + const name = normalizePath(path.relative(config.root, file)) + pluginContext.emitFile({ + name, + fileName, + type: 'asset', + source: content + }) + emittedSet.add(contentHash) + } + + return `__VITE_ASSET__${contentHash}__${postfix ? `$_${postfix}__` : ``}` // TODO_BASE +} + export async function urlToBuiltUrl( url: string, importer: string, diff --git a/playground/css/vite.config-relative-base.js b/playground/css/vite.config-relative-base.js index ae09766c0768ac..33c399c19e6298 100644 --- a/playground/css/vite.config-relative-base.js +++ b/playground/css/vite.config-relative-base.js @@ -2,6 +2,7 @@ * @type {import('vite').UserConfig} */ +let totalSize = 0 const baseConfig = require('./vite.config.js') module.exports = { ...baseConfig, @@ -11,7 +12,10 @@ module.exports = { outDir: 'dist/relative-base', watch: false, minify: false, - assetsInlineLimit: 0, + assetsInlineLimit: (_file, fileSize, combinedSize) => { + totalSize += fileSize + return true && totalSize === combinedSize + fileSize + }, rollupOptions: { output: { entryFileNames: 'entries/[name].js', diff --git a/playground/wasm/vite.config.ts b/playground/wasm/vite.config.ts index 43833d2f95d302..884b48699df72d 100644 --- a/playground/wasm/vite.config.ts +++ b/playground/wasm/vite.config.ts @@ -3,6 +3,8 @@ export default defineConfig({ build: { // make can no emit light.wasm // and emit add.wasm - assetsInlineLimit: 80 + assetsInlineLimit: (_file: string, _size: number, _totalSize: number) => { + return true + } } })