From 2166b76c53134f543d550152f2f38b54e659c27d Mon Sep 17 00:00:00 2001 From: Sergey Tatarintsev Date: Thu, 1 Jul 2021 20:25:06 +0200 Subject: [PATCH] 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 --- package-lock.json | 21 +----------- package.json | 2 +- package.postcss7.json | 2 +- src/lib/purgeUnusedStyles.js | 56 +++++++++++++++++++++++------- tests/purgeUnusedStyles.test.js | 61 +++++++++++++++++++++++++++++++++ 5 files changed, 108 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index b93e75d60c00..cee09f359ed2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index badfeda87262..aef6cbfa95e0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/package.postcss7.json b/package.postcss7.json index b6eb6d996b91..e5ca69d2324f 100644 --- a/package.postcss7.json +++ b/package.postcss7.json @@ -3,7 +3,7 @@ "cssnano": "^4" }, "dependencies": { - "@fullhuman/postcss-purgecss": "^3.1.3", + "purgecss": "^4.0.3", "autoprefixer": "^9", "postcss": "^7", "postcss-functions": "^3", diff --git a/src/lib/purgeUnusedStyles.js b/src/lib/purgeUnusedStyles.js index fb452b058645..23b77d197de5 100644 --- a/src/lib/purgeUnusedStyles.js +++ b/src/lib/purgeUnusedStyles.js @@ -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() + }, ]) } diff --git a/tests/purgeUnusedStyles.test.js b/tests/purgeUnusedStyles.test.js index 71b3c8a2e77e..20411061253b 100644 --- a/tests/purgeUnusedStyles.test.js +++ b/tests/purgeUnusedStyles.test.js @@ -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(() => {