From 4b15b90f219b4c3d09d26525241f7304a1a29434 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 16 Aug 2020 13:56:21 +0200 Subject: [PATCH 01/17] add perf utils --- .gitignore | 3 ++ perf/.gitignore | 2 ++ perf/fixture.css | 61 +++++++++++++++++++++++++++++++++++++++++ perf/script.sh | 15 ++++++++++ perf/tailwind.config.js | 28 +++++++++++++++++++ 5 files changed, 109 insertions(+) create mode 100644 perf/.gitignore create mode 100644 perf/fixture.css create mode 100755 perf/script.sh create mode 100644 perf/tailwind.config.js diff --git a/.gitignore b/.gitignore index ca77e406f1ed..6167575ed507 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ index.html package-lock.json yarn-error.log + +# Perf related files +isolate*.log \ No newline at end of file diff --git a/perf/.gitignore b/perf/.gitignore new file mode 100644 index 000000000000..bc7a91cc4008 --- /dev/null +++ b/perf/.gitignore @@ -0,0 +1,2 @@ +output*.css +v8.json \ No newline at end of file diff --git a/perf/fixture.css b/perf/fixture.css new file mode 100644 index 000000000000..8de93d6fb64d --- /dev/null +++ b/perf/fixture.css @@ -0,0 +1,61 @@ +@tailwind base; + +@tailwind components; + +@tailwind utilities; + + +.btn-1-xl { + @apply sm:space-x-0; + @apply xl:space-x-0; + @apply sm:space-x-1; + @apply xl:space-x-1; + @apply sm:space-y-0; + @apply xl:space-y-0; + @apply sm:space-y-1; + @apply xl:space-y-1; +} +.btn-2-xl { + @apply sm:space-x-0; + @apply xl:space-x-0; + @apply sm:space-x-1; + @apply xl:space-x-1; + @apply sm:space-y-0; + @apply xl:space-y-0; + @apply sm:space-y-1; + @apply xl:space-y-1; + @apply btn-1-xl; +} +.btn-3-xl { + @apply sm:space-x-0; + @apply xl:space-x-0; + @apply sm:space-x-1; + @apply xl:space-x-1; + @apply sm:space-y-0; + @apply xl:space-y-0; + @apply sm:space-y-1; + @apply xl:space-y-1; + @apply btn-2-xl; +} +.btn-4-xl { + @apply sm:space-x-0; + @apply xl:space-x-0; + @apply sm:space-x-1; + @apply xl:space-x-1; + @apply sm:space-y-0; + @apply xl:space-y-0; + @apply sm:space-y-1; + @apply xl:space-y-1; + @apply btn-3-xl; +} +.btn-5-xl { + @apply sm:space-x-0; + @apply xl:space-x-0; + @apply sm:space-x-1; + @apply xl:space-x-1; + @apply sm:space-y-0; + @apply xl:space-y-0; + @apply sm:space-y-1; + @apply xl:space-y-1; + @apply btn-4-xl; +} diff --git a/perf/script.sh b/perf/script.sh new file mode 100755 index 000000000000..cad09f2f38ca --- /dev/null +++ b/perf/script.sh @@ -0,0 +1,15 @@ +# Cleanup existing perf stuff +rm isolate-*.log + +# Ensure we use the latest build version +npm run babelify + +# Run Tailwind on the big fixture file & profile it +node --prof lib/cli.js build ./perf/fixture.css -c ./perf/tailwind.config.js -o ./perf/output.css + +# Generate flame graph +node --prof-process --preprocess -j isolate*.log > ./perf/v8.json + +# Now visit: https://mapbox.github.io/flamebearer/ +# And drag that v8.json file in there! +# You can put "./lib" in the search box which will highlight all our code in green. \ No newline at end of file diff --git a/perf/tailwind.config.js b/perf/tailwind.config.js new file mode 100644 index 000000000000..7fb2125aae9e --- /dev/null +++ b/perf/tailwind.config.js @@ -0,0 +1,28 @@ +module.exports = { + future: 'all', + experimental: 'all', + purge: [], + theme: { + extend: {}, + }, + variants: [ + 'responsive', + 'motion-safe', + 'motion-reduce', + 'group-hover', + 'group-focus', + 'hover', + 'focus-within', + 'focus-visible', + 'focus', + 'active', + 'visited', + 'disabled', + 'checked', + 'first', + 'last', + 'odd', + 'even', + ], + plugins: [], +} From 8c7fb84e581ecaac85cd8912c30905899a967e4c Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 16 Aug 2020 14:03:37 +0200 Subject: [PATCH 02/17] bail out of the applyComplexClasses when it is not needed There is no need in re-compiling tailwind or building expensive lookupt tables when it turns out that we don't even need it. We have a small overhead by walking the tree to check if `@apply` exists. However this outweighs the slowness of re-generating tailwind + expensive lookup tables. --- src/flagged/applyComplexClasses.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index 9b1d7e8f706d..17e335a2731c 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -236,6 +236,11 @@ function processApplyAtRules(css, lookupTree, config) { export default function applyComplexClasses(config, getProcessedPlugins) { return function(css) { + // We can stop already when we don't have any @apply rules. Vue users: you're welcome! + if (!hasAtRule(css, 'apply')) { + return css + } + // Tree already contains @tailwind rules, don't prepend default Tailwind tree if (hasAtRule(css, 'tailwind')) { return processApplyAtRules(css, css, config) From aa7ae6af37668a2d627a5183d3b6acee804ad588 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 16 Aug 2020 14:06:32 +0200 Subject: [PATCH 03/17] re-use classNameParser We were re-creating the classNameParser inside the loop. Since that code is all pretty pure we can hoist it so that we don't have to recreate that parser all the time. --- src/util/generateVariantFunction.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/util/generateVariantFunction.js b/src/util/generateVariantFunction.js index 67b8fe2306fb..86b2dc64cb50 100644 --- a/src/util/generateVariantFunction.js +++ b/src/util/generateVariantFunction.js @@ -2,6 +2,10 @@ import _ from 'lodash' import postcss from 'postcss' import selectorParser from 'postcss-selector-parser' +const classNameParser = selectorParser(selectors => { + return selectors.first.filter(({ type }) => type === 'class').pop().value +}) + export default function generateVariantFunction(generator) { return (container, config) => { const cloned = postcss.root({ nodes: container.clone().nodes }) @@ -18,9 +22,7 @@ export default function generateVariantFunction(generator) { } rule.selectors = rule.selectors.map(selector => { - const className = selectorParser(selectors => { - return selectors.first.filter(({ type }) => type === 'class').pop().value - }).transformSync(selector) + const className = classNameParser.transformSync(selector) return modifierFunction({ className, From 5564e0b493d8da433e3d3fddb437750029381b9e Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 16 Aug 2020 14:09:47 +0200 Subject: [PATCH 04/17] remove unused containsApply check Currently we will walk the tree for every single rule to see if an `@apply` exists somewhere in that tree. However we don't use the `containsApply` anymore so this is a quick win! --- src/flagged/applyComplexClasses.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index 17e335a2731c..124d91da5de6 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -90,7 +90,6 @@ function buildUtilityMap(css) { utilityName, classPosition: i, rule: rule.clone({ parent: rule.parent }), - containsApply: hasAtRule(rule, 'apply'), }) index++ }) From f2e3e22c6b5bf217a59a7e1c11e389447dd7cfe8 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 16 Aug 2020 14:11:19 +0200 Subject: [PATCH 05/17] make the cloning of the rule in the lookup table lazy We create a big lookup table so that we can lookup the nodes by its utilityName. This is used inside the recursive `@apply` code. This big lookup table will clone every single rule and put it in, however we don't need to clone everything! We are only interested in the rules that have been actually applied. This way we make the cloning of the rule lazy and only when we use this exact rule. There is an additional performace "issue" though: When we read the same rule multiple times, it will clone every time you read from that object. We could add additional memoization stuff, but so far it doesn't seem to be the bottleneck. Therefore I've added a perf todo just to leave a mark when this becomes the bottleneck. --- src/flagged/applyComplexClasses.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index 124d91da5de6..62487a3f341c 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -89,7 +89,11 @@ function buildUtilityMap(css) { index, utilityName, classPosition: i, - rule: rule.clone({ parent: rule.parent }), + get rule() { + // TODO: #perf every time we "read" this value we will create a copy. + // Is this an issue? + return rule.clone({ parent: rule.parent }) + }, }) index++ }) From 88888fd0f8c30f7cb2e59c36bb3de0b83b93b7b9 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 16 Aug 2020 14:30:57 +0200 Subject: [PATCH 06/17] switch to a `do {} while ()` We alreayd know that we have an `@apply` otherwise we would not have called that function in the first place. Moving to a `do {} while ()` allows us to skip 1 call to `hasAtRule(css, 'apply')`. Which is nice because that skips a possible full traversal. --- src/flagged/applyComplexClasses.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index 62487a3f341c..aec9065a1ece 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -173,7 +173,7 @@ function makeExtractUtilityRules(css, config) { function processApplyAtRules(css, lookupTree, config) { const extractUtilityRules = makeExtractUtilityRules(lookupTree, config) - while (hasAtRule(css, 'apply')) { + do { css.walkRules(rule => { const applyRules = [] @@ -232,7 +232,18 @@ function processApplyAtRules(css, lookupTree, config) { rule.remove() } }) - } + + // We already know that we have at least 1 @apply rule. Otherwise this + // function would not have been called. Therefore we can execute this code + // at least once. This also means that in the best case scenario we only + // call this 2 times, instead of 3 times. + // 1st time -> before we call this function + // 2nd time -> when we check if we have to do this loop again (because do {} while (check)) + // .. instead of + // 1st time -> before we call this function + // 2nd time -> when we check the first time (because while (check) do {}) + // 3rd time -> when we re-check to see if we should do this loop again + } while (hasAtRule(css, 'apply')) return css } From e39fd6f2b3bf7f31fa37d2a3eebe5c245113ad46 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 16 Aug 2020 14:32:42 +0200 Subject: [PATCH 07/17] remove the reversed orderedUtilityMap We don't require this reversed map since we can already sort by the index on the node directly. Therefore this can be dropped. --- src/flagged/applyComplexClasses.js | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index aec9065a1ece..52ec03a753d5 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -139,15 +139,11 @@ function mergeAdjacentRules(initialRule, rulesToInsert) { function makeExtractUtilityRules(css, config) { const utilityMap = buildUtilityMap(css) - const orderUtilityMap = _.fromPairs( - _.flatMap(_.toPairs(utilityMap), ([_utilityName, utilities]) => { - return utilities.map(utility => { - return [utility.index, utility] - }) - }) - ) - return function(utilityNames, rule) { - return _.flatMap(utilityNames, utilityName => { + + return function extractUtilityRules(utilityNames, rule) { + const combined = [] + + utilityNames.forEach(utilityName => { if (utilityMap[utilityName] === undefined) { // Look for prefixed utility in case the user has goofed const prefixedUtility = prefixSelector(config.prefix, `.${utilityName}`).slice(1) @@ -163,10 +159,11 @@ function makeExtractUtilityRules(css, config) { { word: utilityName } ) } - return utilityMap[utilityName].map(({ index }) => index) + + combined.push(...utilityMap[utilityName]) }) - .sort((a, b) => a - b) - .map(i => orderUtilityMap[i]) + + return combined.sort((a, b) => a.index - b.index) } } From 309b8e5bb854864dc95ea068292c5cffb1d4ce1a Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 16 Aug 2020 14:38:53 +0200 Subject: [PATCH 08/17] re-use the same tailwindApplyPlaceholder --- src/flagged/applyComplexClasses.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index 52ec03a753d5..e2fbfd758e26 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -20,15 +20,18 @@ function hasAtRule(css, atRule) { return foundAtRule } +const tailwindApplyPlaceholder = selectorParser.attribute({ + attribute: '__TAILWIND-APPLY-PLACEHOLDER__', +}) + function generateRulesFromApply({ rule, utilityName: className, classPosition }, replaceWith) { const processedSelectors = rule.selectors.map(selector => { const processor = selectorParser(selectors => { let i = 0 selectors.walkClasses(c => { - if (c.value === className && classPosition === i) { - c.replaceWith(selectorParser.attribute({ attribute: '__TAILWIND-APPLY-PLACEHOLDER__' })) + if (classPosition === i++ && c.value === className) { + c.replaceWith(tailwindApplyPlaceholder) } - i++ }) }) From 78df10020fbc1b4844358277cd1b15dca0e08cca Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 16 Aug 2020 14:39:35 +0200 Subject: [PATCH 09/17] hoist the selector parser No need to re-create the selector parser in the loop for each selector. --- src/flagged/applyComplexClasses.js | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index e2fbfd758e26..11cb3237ae81 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -25,24 +25,20 @@ const tailwindApplyPlaceholder = selectorParser.attribute({ }) function generateRulesFromApply({ rule, utilityName: className, classPosition }, replaceWith) { - const processedSelectors = rule.selectors.map(selector => { - const processor = selectorParser(selectors => { - let i = 0 - selectors.walkClasses(c => { - if (classPosition === i++ && c.value === className) { - c.replaceWith(tailwindApplyPlaceholder) - } - }) + const processor = selectorParser(selectors => { + let i = 0 + selectors.walkClasses(c => { + if (classPosition === i++ && c.value === className) { + c.replaceWith(tailwindApplyPlaceholder) + } }) + }) + const processedSelectors = rule.selectors.map(selector => { // You could argue we should make this replacement at the AST level, but if we believe // the placeholder string is safe from collisions then it is safe to do this is a simple // string replacement, and much, much faster. - const processedSelector = processor - .processSync(selector) - .replace('[__TAILWIND-APPLY-PLACEHOLDER__]', replaceWith) - - return processedSelector + return processor.processSync(selector).replace('[__TAILWIND-APPLY-PLACEHOLDER__]', replaceWith) }) const cloned = rule.clone() From 00f4427ea9bb18f0452fea964b3a377b37a449ac Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 16 Aug 2020 14:40:46 +0200 Subject: [PATCH 10/17] improvement cloning of the parent node We used to clone the full tree and then remove all the children, this was a bit too slow so therefore we will now create a new tree based on the old information. --- src/flagged/applyComplexClasses.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index 11cb3237ae81..8d8f7194c3b8 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -20,6 +20,20 @@ function hasAtRule(css, atRule) { return foundAtRule } +function cloneWithoutChildren(node) { + if (node.type === 'atrule') { + return postcss.atRule({ name: node.name, params: node.params }) + } + + if (node.type === 'rule') { + return postcss.rule({ name: node.name, selectors: node.selectors }) + } + + const clone = node.clone() + clone.removeAll() + return clone +} + const tailwindApplyPlaceholder = selectorParser.attribute({ attribute: '__TAILWIND-APPLY-PLACEHOLDER__', }) @@ -46,8 +60,8 @@ function generateRulesFromApply({ rule, utilityName: className, classPosition }, let parent = rule.parent while (parent && parent.type !== 'root') { - const parentClone = parent.clone() - parentClone.removeAll() + const parentClone = cloneWithoutChildren(parent) + parentClone.append(current) current.parent = parentClone current = parentClone From 0631851b7bea5ec2a3a548098dd3437683cfca49 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 16 Aug 2020 14:48:00 +0200 Subject: [PATCH 11/17] introduce a useMemo utility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Naming is hard so I took this name from the React hook 😎 Also use this useMemoy utility to make sure that the extractUtilityNames is cached. There is no need to re-compute the utility names all the time. --- src/flagged/applyComplexClasses.js | 26 ++++++++++++-------------- src/util/useMemo.js | 16 ++++++++++++++++ 2 files changed, 28 insertions(+), 14 deletions(-) create mode 100644 src/util/useMemo.js diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index 8d8f7194c3b8..ee710d91d59b 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -8,6 +8,7 @@ import substituteResponsiveAtRules from '../lib/substituteResponsiveAtRules' import convertLayerAtRulesToControlComments from '../lib/convertLayerAtRulesToControlComments' import substituteScreenAtRules from '../lib/substituteScreenAtRules' import prefixSelector from '../util/prefixSelector' +import { useMemo } from '../util/useMemo' function hasAtRule(css, atRule) { let foundAtRule = false @@ -39,7 +40,7 @@ const tailwindApplyPlaceholder = selectorParser.attribute({ }) function generateRulesFromApply({ rule, utilityName: className, classPosition }, replaceWith) { - const processor = selectorParser(selectors => { + const parser = selectorParser(selectors => { let i = 0 selectors.walkClasses(c => { if (classPosition === i++ && c.value === className) { @@ -52,7 +53,7 @@ function generateRulesFromApply({ rule, utilityName: className, classPosition }, // You could argue we should make this replacement at the AST level, but if we believe // the placeholder string is safe from collisions then it is safe to do this is a simple // string replacement, and much, much faster. - return processor.processSync(selector).replace('[__TAILWIND-APPLY-PLACEHOLDER__]', replaceWith) + return parser.processSync(selector).replace('[__TAILWIND-APPLY-PLACEHOLDER__]', replaceWith) }) const cloned = rule.clone() @@ -72,19 +73,16 @@ function generateRulesFromApply({ rule, utilityName: className, classPosition }, return current } -function extractUtilityNames(selector) { - const processor = selectorParser(selectors => { - let classes = [] - - selectors.walkClasses(c => { - classes.push(c) - }) - - return classes.map(c => c.value) - }) +const extractUtilityNamesParser = selectorParser(selectors => { + let classes = [] + selectors.walkClasses(c => classes.push(c.value)) + return classes +}) - return processor.transformSync(selector) -} +const extractUtilityNames = useMemo( + selector => extractUtilityNamesParser.transformSync(selector), + selector => selector +) function buildUtilityMap(css) { let index = 0 diff --git a/src/util/useMemo.js b/src/util/useMemo.js new file mode 100644 index 000000000000..a70c397f05fb --- /dev/null +++ b/src/util/useMemo.js @@ -0,0 +1,16 @@ +export function useMemo(cb, keyResolver) { + const cache = new Map() + + return (...args) => { + const key = keyResolver(...args) + + if (cache.has(key)) { + return cache.get(key) + } + + const result = cb(...args) + cache.set(key, result) + + return result + } +} From fab4d7b8f635d5ebaf33a23a8e45cca66096cbc3 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 16 Aug 2020 15:24:35 +0200 Subject: [PATCH 12/17] cache buildSelectorVariant --- src/util/buildSelectorVariant.js | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/util/buildSelectorVariant.js b/src/util/buildSelectorVariant.js index a7c938f458fc..77c0b680d9e3 100644 --- a/src/util/buildSelectorVariant.js +++ b/src/util/buildSelectorVariant.js @@ -1,15 +1,21 @@ import parser from 'postcss-selector-parser' import tap from 'lodash/tap' +import { useMemo } from './useMemo' -export default function buildSelectorVariant(selector, variantName, separator, onError = () => {}) { - return parser(selectors => { - tap(selectors.first.filter(({ type }) => type === 'class').pop(), classSelector => { - if (classSelector === undefined) { - onError('Variant cannot be generated because selector contains no classes.') - return - } +const buildSelectorVariant = useMemo( + (selector, variantName, separator, onError = () => {}) => { + return parser(selectors => { + tap(selectors.first.filter(({ type }) => type === 'class').pop(), classSelector => { + if (classSelector === undefined) { + onError('Variant cannot be generated because selector contains no classes.') + return + } - classSelector.value = `${variantName}${separator}${classSelector.value}` - }) - }).processSync(selector) -} + classSelector.value = `${variantName}${separator}${classSelector.value}` + }) + }).processSync(selector) + }, + (selector, variantName, separator) => [selector, variantName, separator].join('||') +) + +export default buildSelectorVariant From 8ae2a32a0c1277b3f18dda6e214718d7c81b6972 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 16 Aug 2020 16:54:35 +0200 Subject: [PATCH 13/17] hoist selectorParser setup code No need to re-create the selectorParser in every call. --- src/lib/substituteVariantsAtRules.js | 79 ++++++++++++++-------------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/src/lib/substituteVariantsAtRules.js b/src/lib/substituteVariantsAtRules.js index 578d5734341a..1cf189559773 100644 --- a/src/lib/substituteVariantsAtRules.js +++ b/src/lib/substituteVariantsAtRules.js @@ -6,14 +6,14 @@ import prefixSelector from '../util/prefixSelector' function generatePseudoClassVariant(pseudoClass, selectorPrefix = pseudoClass) { return generateVariantFunction(({ modifySelectors, separator }) => { - return modifySelectors(({ selector }) => { - return selectorParser(selectors => { - selectors.walkClasses(sel => { - sel.value = `${selectorPrefix}${separator}${sel.value}` - sel.parent.insertAfter(sel, selectorParser.pseudo({ value: `:${pseudoClass}` })) - }) - }).processSync(selector) + const parser = selectorParser(selectors => { + selectors.walkClasses(sel => { + sel.value = `${selectorPrefix}${separator}${sel.value}` + sel.parent.insertAfter(sel, selectorParser.pseudo({ value: `:${pseudoClass}` })) + }) }) + + return modifySelectors(({ selector }) => parser.processSync(selector)) }) } @@ -24,13 +24,12 @@ function ensureIncludesDefault(variants) { const defaultVariantGenerators = config => ({ default: generateVariantFunction(() => {}), 'motion-safe': generateVariantFunction(({ container, separator, modifySelectors }) => { - const modified = modifySelectors(({ selector }) => { - return selectorParser(selectors => { - selectors.walkClasses(sel => { - sel.value = `motion-safe${separator}${sel.value}` - }) - }).processSync(selector) + const parser = selectorParser(selectors => { + selectors.walkClasses(sel => { + sel.value = `motion-safe${separator}${sel.value}` + }) }) + const modified = modifySelectors(({ selector }) => parser.processSync(selector)) const mediaQuery = postcss.atRule({ name: 'media', params: '(prefers-reduced-motion: no-preference)', @@ -39,42 +38,42 @@ const defaultVariantGenerators = config => ({ container.append(mediaQuery) }), 'motion-reduce': generateVariantFunction(({ container, separator, modifySelectors }) => { - const modified = modifySelectors(({ selector }) => { - return selectorParser(selectors => { - selectors.walkClasses(sel => { - sel.value = `motion-reduce${separator}${sel.value}` - }) - }).processSync(selector) + const parser = selectorParser(selectors => { + selectors.walkClasses(sel => { + sel.value = `motion-reduce${separator}${sel.value}` + }) + }) + const modified = modifySelectors(({ selector }) => parser.processSync(selector)) + const mediaQuery = postcss.atRule({ + name: 'media', + params: '(prefers-reduced-motion: reduce)', }) - const mediaQuery = postcss.atRule({ name: 'media', params: '(prefers-reduced-motion: reduce)' }) mediaQuery.append(modified) container.append(mediaQuery) }), 'group-hover': generateVariantFunction(({ modifySelectors, separator }) => { - return modifySelectors(({ selector }) => { - return selectorParser(selectors => { - selectors.walkClasses(sel => { - sel.value = `group-hover${separator}${sel.value}` - sel.parent.insertBefore( - sel, - selectorParser().astSync(prefixSelector(config.prefix, '.group:hover ')) - ) - }) - }).processSync(selector) + const parser = selectorParser(selectors => { + selectors.walkClasses(sel => { + sel.value = `group-hover${separator}${sel.value}` + sel.parent.insertBefore( + sel, + selectorParser().astSync(prefixSelector(config.prefix, '.group:hover ')) + ) + }) }) + return modifySelectors(({ selector }) => parser.processSync(selector)) }), 'group-focus': generateVariantFunction(({ modifySelectors, separator }) => { - return modifySelectors(({ selector }) => { - return selectorParser(selectors => { - selectors.walkClasses(sel => { - sel.value = `group-focus${separator}${sel.value}` - sel.parent.insertBefore( - sel, - selectorParser().astSync(prefixSelector(config.prefix, '.group:focus ')) - ) - }) - }).processSync(selector) + const parser = selectorParser(selectors => { + selectors.walkClasses(sel => { + sel.value = `group-focus${separator}${sel.value}` + sel.parent.insertBefore( + sel, + selectorParser().astSync(prefixSelector(config.prefix, '.group:focus ')) + ) + }) }) + return modifySelectors(({ selector }) => parser.processSync(selector)) }), hover: generatePseudoClassVariant('hover'), 'focus-within': generatePseudoClassVariant('focus-within'), From 5260c71c60f46d1705914758607900d497887508 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Sun, 16 Aug 2020 17:59:08 +0200 Subject: [PATCH 14/17] only parse the className when needed --- src/util/generateVariantFunction.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/util/generateVariantFunction.js b/src/util/generateVariantFunction.js index 86b2dc64cb50..945a6c80782f 100644 --- a/src/util/generateVariantFunction.js +++ b/src/util/generateVariantFunction.js @@ -22,10 +22,10 @@ export default function generateVariantFunction(generator) { } rule.selectors = rule.selectors.map(selector => { - const className = classNameParser.transformSync(selector) - return modifierFunction({ - className, + get className() { + return classNameParser.transformSync(selector) + }, selector, }) }) From e417da262e8986c5b172df815e9f94ac1a8463c3 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 18 Aug 2020 14:08:55 +0200 Subject: [PATCH 15/17] cache clone rule Otherwise every time we read this value it will be re-cloned --- src/flagged/applyComplexClasses.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index ee710d91d59b..7234fa2466b6 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -84,6 +84,11 @@ const extractUtilityNames = useMemo( selector => selector ) +const cloneRuleWithParent = useMemo( + rule => rule.clone({ parent: rule.parent }), + rule => rule +) + function buildUtilityMap(css) { let index = 0 const utilityMap = {} @@ -101,9 +106,7 @@ function buildUtilityMap(css) { utilityName, classPosition: i, get rule() { - // TODO: #perf every time we "read" this value we will create a copy. - // Is this an issue? - return rule.clone({ parent: rule.parent }) + return cloneRuleWithParent(rule) }, }) index++ From fe70c897181cbcc813b1ffd23eb1aa222179895a Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 18 Aug 2020 14:09:37 +0200 Subject: [PATCH 16/17] use append instead of prepend Same idea, but prepend will internally reverse all nodes. --- src/flagged/applyComplexClasses.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/flagged/applyComplexClasses.js b/src/flagged/applyComplexClasses.js index 7234fa2466b6..fe1ba121799d 100644 --- a/src/flagged/applyComplexClasses.js +++ b/src/flagged/applyComplexClasses.js @@ -291,7 +291,7 @@ export default function applyComplexClasses(config, getProcessedPlugins) { ) .then(result => { // Prepend Tailwind's generated classes to the tree so they are available for `@apply` - const lookupTree = _.tap(css.clone(), tree => tree.prepend(result.root)) + const lookupTree = _.tap(result.root, tree => tree.append(css.clone())) return processApplyAtRules(css, lookupTree, config) }) } From 33ee64665dcf919b640557c9db657b8e038bed8e Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Tue, 18 Aug 2020 14:10:28 +0200 Subject: [PATCH 17/17] cache className resolve --- src/util/generateVariantFunction.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/util/generateVariantFunction.js b/src/util/generateVariantFunction.js index 945a6c80782f..8de4e4867a7f 100644 --- a/src/util/generateVariantFunction.js +++ b/src/util/generateVariantFunction.js @@ -1,11 +1,17 @@ import _ from 'lodash' import postcss from 'postcss' import selectorParser from 'postcss-selector-parser' +import { useMemo } from './useMemo' const classNameParser = selectorParser(selectors => { return selectors.first.filter(({ type }) => type === 'class').pop().value }) +const getClassNameFromSelector = useMemo( + selector => classNameParser.transformSync(selector), + selector => selector +) + export default function generateVariantFunction(generator) { return (container, config) => { const cloned = postcss.root({ nodes: container.clone().nodes }) @@ -24,7 +30,7 @@ export default function generateVariantFunction(generator) { rule.selectors = rule.selectors.map(selector => { return modifierFunction({ get className() { - return classNameParser.transformSync(selector) + return getClassNameFromSelector(selector) }, selector, })