Merge pull request #2171 from tailwindlabs/perf-improvements

Performance improvements
This commit is contained in:
Adam Wathan 2020-08-18 08:28:42 -04:00 committed by GitHub
commit 9f9065ddb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 270 additions and 102 deletions

3
.gitignore vendored
View File

@ -4,3 +4,6 @@
index.html
package-lock.json
yarn-error.log
# Perf related files
isolate*.log

2
perf/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
output*.css
v8.json

61
perf/fixture.css Normal file
View File

@ -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;
}

15
perf/script.sh Executable file
View File

@ -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.

28
perf/tailwind.config.js Normal file
View File

@ -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: [],
}

View File

@ -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
@ -20,26 +21,39 @@ function hasAtRule(css, atRule) {
return foundAtRule
}
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__' }))
}
i++
})
})
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__',
})
function generateRulesFromApply({ rule, utilityName: className, classPosition }, replaceWith) {
const parser = 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 parser.processSync(selector).replace('[__TAILWIND-APPLY-PLACEHOLDER__]', replaceWith)
})
const cloned = rule.clone()
@ -47,8 +61,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
@ -59,19 +73,21 @@ function generateRulesFromApply({ rule, utilityName: className, classPosition },
return current
}
function extractUtilityNames(selector) {
const processor = selectorParser(selectors => {
let classes = []
const extractUtilityNamesParser = selectorParser(selectors => {
let classes = []
selectors.walkClasses(c => classes.push(c.value))
return classes
})
selectors.walkClasses(c => {
classes.push(c)
})
const extractUtilityNames = useMemo(
selector => extractUtilityNamesParser.transformSync(selector),
selector => selector
)
return classes.map(c => c.value)
})
return processor.transformSync(selector)
}
const cloneRuleWithParent = useMemo(
rule => rule.clone({ parent: rule.parent }),
rule => rule
)
function buildUtilityMap(css) {
let index = 0
@ -89,8 +105,9 @@ function buildUtilityMap(css) {
index,
utilityName,
classPosition: i,
rule: rule.clone({ parent: rule.parent }),
containsApply: hasAtRule(rule, 'apply'),
get rule() {
return cloneRuleWithParent(rule)
},
})
index++
})
@ -136,15 +153,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)
@ -160,17 +173,18 @@ 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)
}
}
function processApplyAtRules(css, lookupTree, config) {
const extractUtilityRules = makeExtractUtilityRules(lookupTree, config)
while (hasAtRule(css, 'apply')) {
do {
css.walkRules(rule => {
const applyRules = []
@ -229,13 +243,29 @@ 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
}
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)
@ -261,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)
})
}

View File

@ -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'),

View File

@ -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

View File

@ -1,6 +1,16 @@
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) => {
@ -18,12 +28,10 @@ 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)
return modifierFunction({
className,
get className() {
return getClassNameFromSelector(selector)
},
selector,
})
})

16
src/util/useMemo.js Normal file
View File

@ -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
}
}