Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(css): shrink base64 usage in css file #12517

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 79 additions & 8 deletions packages/vite/src/node/plugins/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ const postcssConfigCache = new WeakMap<
PostCSSConfigResult | null | Promise<PostCSSConfigResult | null>
>()

const rootCssVariableCache = new WeakMap<ResolvedConfig, Map<string, string>>()

function encodePublicUrlsInCSS(config: ResolvedConfig) {
return config.command === 'build'
}
Expand All @@ -182,6 +184,8 @@ function encodePublicUrlsInCSS(config: ResolvedConfig) {
export function cssPlugin(config: ResolvedConfig): Plugin {
let server: ViteDevServer
let moduleCache: Map<string, Record<string, string>>
let rootVarsCache: Map<string, string>
let rootVarsEmitted: boolean

const resolveUrl = config.createResolver({
preferRelative: true,
Expand All @@ -204,6 +208,10 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
moduleCache = new Map<string, Record<string, string>>()
cssModulesCache.set(config, moduleCache)

rootVarsCache = new Map<string, string>()
rootCssVariableCache.set(config, rootVarsCache)
rootVarsEmitted = false

removedPureCssFilesCache.set(config, new Map<string, RenderedChunk>())
},

Expand All @@ -216,6 +224,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
return
}
const ssr = options?.ssr === true
const isBuild = config.command === 'build'

const urlReplacer: CssUrlReplacer = async (url, importer) => {
if (checkPublicFile(url, config)) {
Expand All @@ -229,7 +238,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
if (resolved) {
return fileToUrl(resolved, config, this)
}
if (config.command === 'build') {
if (isBuild) {
// #9800 If we cannot resolve the css url, leave a warning.
config.logger.warnOnce(
`\n${url} referenced in ${id} didn't resolve at build time, it will remain unchanged to be resolved at runtime`,
Expand All @@ -243,13 +252,17 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
modules,
deps,
map,
} = await compileCSS(id, raw, config, urlReplacer)
} = await compileCSS(id, raw, config, {
urlReplacer,
rootVarsCache,
isBuild,
})
if (modules) {
moduleCache.set(id, modules)
}

// track deps for build watch mode
if (config.command === 'build' && config.build.watch && deps) {
if (isBuild && config.build.watch && deps) {
for (const file of deps) {
this.addWatchFile(file)
}
Expand Down Expand Up @@ -307,6 +320,25 @@ export function cssPlugin(config: ResolvedConfig): Plugin {
map,
}
},

async renderChunk(_, chunk) {
if (!rootVarsEmitted && rootVarsCache.size) {
const rootVarsCss = joinRootVars(rootVarsCache)
chunk.viteMetadata!.importedCss.add(
this.getFileName(
this.emitFile({
name: 'root-css-variable.css',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it better to inject the css in the css chunk, because this css will biger and biger. and the user can't do the loading on demand

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a supplement, the index.html will load 100 assets, but it only need the 10 assets

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I think further, it would be better to keep the transform behavior both in dev and build mode. Otherwise, it may break some plugins that depend on base64 during the transform phase.

Duplicate base64 CSS variables in a CSS chunk will be removed in css-post plugin's renderChunk phase.

type: 'asset',
source: config.build.cssMinify
? await minifyCSS(rootVarsCss, config)
: rootVarsCss,
}),
),
)
rootVarsEmitted = true
}
return null
},
}
}

Expand Down Expand Up @@ -809,7 +841,11 @@ async function compileCSS(
id: string,
code: string,
config: ResolvedConfig,
urlReplacer?: CssUrlReplacer,
options?: {
urlReplacer?: CssUrlReplacer
rootVarsCache: Map<string, string>
isBuild: boolean
},
): Promise<{
code: string
map?: SourceMapInput
Expand Down Expand Up @@ -938,11 +974,13 @@ async function compileCSS(
)
}

if (urlReplacer) {
if (options?.urlReplacer) {
postcssPlugins.push(
UrlRewritePostcssPlugin({
replacer: urlReplacer,
replacer: options.urlReplacer,
logger: config.logger,
rootVars: options.rootVarsCache,
isBuild: options.isBuild,
}),
)
}
Expand Down Expand Up @@ -1236,6 +1274,8 @@ const cssImageSetRE = /(?<=image-set\()((?:[\w\-]{1,256}\([^)]*\)|[^)])*)(?=\))/
const UrlRewritePostcssPlugin: PostCSS.PluginCreator<{
replacer: CssUrlReplacer
logger: Logger
rootVars: Map<string, string>
isBuild: boolean
}> = (opts) => {
if (!opts) {
throw new Error('base or replace is required')
Expand All @@ -1245,6 +1285,7 @@ const UrlRewritePostcssPlugin: PostCSS.PluginCreator<{
postcssPlugin: 'vite-url-rewrite',
Once(root) {
const promises: Promise<void>[] = []
const isCssFile = !root.source?.input.file?.includes('.html')
root.walkDecls((declaration) => {
const importer = declaration.source?.input.file
if (!importer) {
Expand All @@ -1267,20 +1308,50 @@ const UrlRewritePostcssPlugin: PostCSS.PluginCreator<{
promises.push(
rewriterToUse(declaration.value, replacerForDeclaration).then(
(url) => {
declaration.value = url
let match

if (
isCssFile &&
(match = url.match(/url\(['"]?data:.+?;base64,.+?\)/))
) {
const [base64] = match

sun0day marked this conversation as resolved.
Show resolved Hide resolved
const cssVar =
opts.rootVars.get(base64) || `--base64-${getHash(base64)}`
opts.rootVars.set(base64, cssVar)

declaration.value = url.replace(base64, `var(${cssVar})`)
} else {
declaration.value = url
}
},
),
)
}
})
if (promises.length) {
return Promise.all(promises) as any
return Promise.all(promises).then(() => {
// only inject in dev mode
!opts.isBuild &&
isCssFile &&
root.prepend(joinRootVars(opts.rootVars))
}) as any
}
},
}
}
UrlRewritePostcssPlugin.postcss = true

function joinRootVars(rootVars: Map<string, string>) {
return `:root{${Array.from(rootVars.entries()).reduce(
(cssVars, [url, cssVar]) => {
cssVars += `\n${cssVar}: ${url};`
return cssVars
},
'',
)}}`
}

function rewriteCssUrls(
css: string,
replacer: CssUrlReplacer,
Expand Down