Skip to content

Commit 2166b76

Browse files
authored
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
1 parent bf48211 commit 2166b76

5 files changed

+108
-34
lines changed

package-lock.json

+1-20
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@
7070
"postcss": "^8.0.9"
7171
},
7272
"dependencies": {
73-
"@fullhuman/postcss-purgecss": "^4.0.3",
73+
"purgecss": "^4.0.3",
7474
"arg": "^5.0.0",
7575
"bytes": "^3.0.0",
7676
"chalk": "^4.1.1",

package.postcss7.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"cssnano": "^4"
44
},
55
"dependencies": {
6-
"@fullhuman/postcss-purgecss": "^3.1.3",
6+
"purgecss": "^4.0.3",
77
"autoprefixer": "^9",
88
"postcss": "^7",
99
"postcss-functions": "^3",

src/lib/purgeUnusedStyles.js

+44-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import _ from 'lodash'
22
import postcss from 'postcss'
3-
import purgecss from '@fullhuman/postcss-purgecss'
3+
import PurgeCSS, { defaultOptions, standardizeSafelist, mergeExtractorSelectors } from 'purgecss'
44
import log from '../util/log'
55
import htmlTags from 'html-tags'
66
import path from 'path'
@@ -134,10 +134,11 @@ export default function purgeUnusedUtilities(config, configChanged, registerDepe
134134
registerDependency(parseDependency(fileOrGlob))
135135
}
136136

137+
let hasLayers = false
138+
139+
const mode = _.get(config, 'purge.mode', 'layers')
137140
return postcss([
138141
function (css) {
139-
const mode = _.get(config, 'purge.mode', 'layers')
140-
141142
if (!['all', 'layers'].includes(mode)) {
142143
throw new Error('Purge `mode` must be one of `layers` or `all`.')
143144
}
@@ -164,6 +165,7 @@ export default function purgeUnusedUtilities(config, configChanged, registerDepe
164165
switch (comment.text.trim()) {
165166
case `tailwind start ${layer}`:
166167
comment.text = 'purgecss end ignore'
168+
hasLayers = true
167169
break
168170
case `tailwind end ${layer}`:
169171
comment.text = 'purgecss start ignore'
@@ -178,14 +180,44 @@ export default function purgeUnusedUtilities(config, configChanged, registerDepe
178180
css.append(postcss.comment({ text: 'purgecss end ignore' }))
179181
},
180182
removeTailwindMarkers,
181-
purgecss({
182-
defaultExtractor: (content) => {
183-
const transformer = getTransformer(config)
184-
return defaultExtractor(transformer(content))
185-
},
186-
extractors: fileSpecificExtractors,
187-
...purgeOptions,
188-
content,
189-
}),
183+
184+
async function (css) {
185+
if (mode === 'layers' && !hasLayers) {
186+
return
187+
}
188+
const purgeCSS = new PurgeCSS()
189+
purgeCSS.options = {
190+
...defaultOptions,
191+
192+
defaultExtractor: (content) => {
193+
const transformer = getTransformer(config)
194+
return defaultExtractor(transformer(content))
195+
},
196+
extractors: fileSpecificExtractors,
197+
...purgeOptions,
198+
safelist: standardizeSafelist(purgeOptions.safelist),
199+
}
200+
201+
if (purgeCSS.options.variables) {
202+
purgeCSS.variablesStructure.safelist = purgeCSS.options.safelist.variables || []
203+
}
204+
205+
const fileFormatContents = content.filter((o) => typeof o === 'string')
206+
const rawFormatContents = content.filter((o) => typeof o === 'object')
207+
208+
const cssFileSelectors = await purgeCSS.extractSelectorsFromFiles(
209+
fileFormatContents,
210+
purgeCSS.options.extractors
211+
)
212+
const cssRawSelectors = await purgeCSS.extractSelectorsFromString(
213+
rawFormatContents,
214+
purgeCSS.options.extractors
215+
)
216+
const cssSelectors = mergeExtractorSelectors(cssFileSelectors, cssRawSelectors)
217+
purgeCSS.walkThroughCSS(css, cssSelectors)
218+
if (purgeCSS.options.fontFace) purgeCSS.removeUnusedFontFaces()
219+
if (purgeCSS.options.keyframes) purgeCSS.removeUnusedKeyframes()
220+
if (purgeCSS.options.variables) purgeCSS.removeUnusedCSSVariables()
221+
},
190222
])
191223
}

tests/purgeUnusedStyles.test.js

+61
Original file line numberDiff line numberDiff line change
@@ -639,6 +639,67 @@ test(
639639
})
640640
)
641641

642+
test('purges unused css variables in "all" mode', () => {
643+
return inProduction(
644+
suppressConsoleLogs(() => {
645+
return postcss([
646+
tailwind({
647+
...config,
648+
purge: {
649+
mode: 'all',
650+
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
651+
options: {
652+
variables: true,
653+
},
654+
},
655+
}),
656+
])
657+
.process(
658+
`
659+
:root {
660+
--unused-var: 1;
661+
}
662+
`
663+
)
664+
.then((result) => {
665+
expect(result.css).not.toContain('--unused-var')
666+
})
667+
})
668+
)
669+
})
670+
671+
test('respects safelist.variables in "all" mode', () => {
672+
return inProduction(
673+
suppressConsoleLogs(() => {
674+
return postcss([
675+
tailwind({
676+
...config,
677+
purge: {
678+
mode: 'all',
679+
content: [path.resolve(`${__dirname}/fixtures/**/*.html`)],
680+
options: {
681+
variables: true,
682+
safelist: {
683+
variables: ['--unused-var'],
684+
},
685+
},
686+
},
687+
}),
688+
])
689+
.process(
690+
`
691+
:root {
692+
--unused-var: 1;
693+
}
694+
`
695+
)
696+
.then((result) => {
697+
expect(result.css).toContain('--unused-var')
698+
})
699+
})
700+
)
701+
})
702+
642703
test('element selectors are preserved by default', () => {
643704
return inProduction(
644705
suppressConsoleLogs(() => {

0 commit comments

Comments
 (0)