Improve production build performance for the case of many small non-tailwind stylesheets (#4644)

* Improve `purge` performance in layers mode

In layers mode, skip `purgecss` completely if source stylesheet does
not have any tailwind layers. For the legacy codebases with a lot of
non-tailwind stylesheets, it dratically improves the performance of
the production build.

* fix: purgecss should respect safelist.variables
This commit is contained in:
Sergey Tatarintsev 2021-07-01 20:25:06 +02:00 • committed by GitHub
parent bf482112a6
commit 2166b76c53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 108 additions and 34 deletions

21
package-lock.json generated
View file

@ -9,7 +9,6 @@
"version": "2.2.4",
"license": "MIT",
"dependencies": {
"@fullhuman/postcss-purgecss": "^4.0.3",
"arg": "^5.0.0",
"bytes": "^3.0.0",
"chalk": "^4.1.1",
@ -36,6 +35,7 @@
"postcss-selector-parser": "^6.0.6",
"postcss-value-parser": "^4.1.0",
"pretty-hrtime": "^1.0.3",
"purgecss": "^4.0.3",
"quick-lru": "^5.1.1",
"reduce-css-calc": "^2.1.8",
"resolve": "^1.20.0",
@ -1569,17 +1569,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@fullhuman/postcss-purgecss": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-4.0.3.tgz",
"integrity": "sha512-/EnQ9UDWGGqHkn1UKAwSgh+gJHPKmD+Z+5dQ4gWT4qq2NUyez3zqAfZNwFH3eSgmgO+wjTXfhlLchx2M9/K+7Q==",
"dependencies": {
"purgecss": "^4.0.3"
},
"peerDependencies": {
"postcss": "^8.0.0"
}
},
"node_modules/@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
@ -11507,14 +11496,6 @@
}
}
},
"@fullhuman/postcss-purgecss": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@fullhuman/postcss-purgecss/-/postcss-purgecss-4.0.3.tgz",
"integrity": "sha512-/EnQ9UDWGGqHkn1UKAwSgh+gJHPKmD+Z+5dQ4gWT4qq2NUyez3zqAfZNwFH3eSgmgO+wjTXfhlLchx2M9/K+7Q==",
"requires": {
"purgecss": "^4.0.3"
}
},
"@istanbuljs/load-nyc-config": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",

View file

@ -70,7 +70,7 @@
"postcss": "^8.0.9"
},
"dependencies": {
"@fullhuman/postcss-purgecss": "^4.0.3",
"purgecss": "^4.0.3",
"arg": "^5.0.0",
"bytes": "^3.0.0",
"chalk": "^4.1.1",

View file

@ -3,7 +3,7 @@
"cssnano": "^4"
},
"dependencies": {
"@fullhuman/postcss-purgecss": "^3.1.3",
"purgecss": "^4.0.3",
"autoprefixer": "^9",
"postcss": "^7",
"postcss-functions": "^3",

View file

@ -1,6 +1,6 @@
import _ from 'lodash'
import postcss from 'postcss'
import purgecss from '@fullhuman/postcss-purgecss'
import PurgeCSS, { defaultOptions, standardizeSafelist, mergeExtractorSelectors } from 'purgecss'
import log from '../util/log'
import htmlTags from 'html-tags'
import path from 'path'
@ -134,10 +134,11 @@ export default function purgeUnusedUtilities(config, configChanged, registerDepe
registerDependency(parseDependency(fileOrGlob))
}
let hasLayers = false
const mode = _.get(config, 'purge.mode', 'layers')
return postcss([
function (css) {
const mode = _.get(config, 'purge.mode', 'layers')
if (!['all', 'layers'].includes(mode)) {
throw new Error('Purge `mode` must be one of `layers` or `all`.')
}
@ -164,6 +165,7 @@ export default function purgeUnusedUtilities(config, configChanged, registerDepe
switch (comment.text.trim()) {
case `tailwind start ${layer}`:
comment.text = 'purgecss end ignore'
hasLayers = true
break
case `tailwind end ${layer}`:
comment.text = 'purgecss start ignore'
@ -178,14 +180,44 @@ export default function purgeUnusedUtilities(config, configChanged, registerDepe
css.append(postcss.comment({ text: 'purgecss end ignore' }))
},
removeTailwindMarkers,
purgecss({
defaultExtractor: (content) => {
const transformer = getTransformer(config)
return defaultExtractor(transformer(content))
},
extractors: fileSpecificExtractors,
...purgeOptions,
content,
}),
async function (css) {
if (mode === 'layers' && !hasLayers) {
return
}
const purgeCSS = new PurgeCSS()
purgeCSS.options = {
...defaultOptions,
defaultExtractor: (content) => {
const transformer = getTransformer(config)
return defaultExtractor(transformer(content))
},
extractors: fileSpecificExtractors,
...purgeOptions,
safelist: standardizeSafelist(purgeOptions.safelist),
}
if (purgeCSS.options.variables) {
purgeCSS.variablesStructure.safelist = purgeCSS.options.safelist.variables || []
}
const fileFormatContents = content.filter((o) => typeof o === 'string')
const rawFormatContents = content.filter((o) => typeof o === 'object')
const cssFileSelectors = await purgeCSS.extractSelectorsFromFiles(
fileFormatContents,
purgeCSS.options.extractors
)
const cssRawSelectors = await purgeCSS.extractSelectorsFromString(
rawFormatContents,
purgeCSS.options.extractors
)
const cssSelectors = mergeExtractorSelectors(cssFileSelectors, cssRawSelectors)
purgeCSS.walkThroughCSS(css, cssSelectors)
if (purgeCSS.options.fontFace) purgeCSS.removeUnusedFontFaces()
if (purgeCSS.options.keyframes) purgeCSS.removeUnusedKeyframes()
if (purgeCSS.options.variables) purgeCSS.removeUnusedCSSVariables()
},
])
}

View file

@ -639,6 +639,67 @@ test(
})
)
test('purges unused css variables in "all" mode', () => {
return inProduction(
suppressConsoleLogs(() => {
return postcss([
tailwind({
...config,
purge: {
mode: 'all',
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
options: {
variables: true,
},
},
}),
])
.process(
`
:root {
--unused-var: 1;
}
`
)
.then((result) => {
expect(result.css).not.toContain('--unused-var')
})
})
)
})
test('respects safelist.variables in "all" mode', () => {
return inProduction(
suppressConsoleLogs(() => {
return postcss([
tailwind({
...config,
purge: {
mode: 'all',
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
options: {
variables: true,
safelist: {
variables: ['--unused-var'],
},
},
},
}),
])
.process(
`
:root {
--unused-var: 1;
}
`
)
.then((result) => {
expect(result.css).toContain('--unused-var')
})
})
)
})
test('element selectors are preserved by default', () => {
return inProduction(
suppressConsoleLogs(() => {