mirror of
https://github.com/tailwindlabs/tailwindcss.git
synced 2025-12-08 21:36:08 +00:00
Merge pull request #2171 from tailwindlabs/perf-improvements
Performance improvements
This commit is contained in:
commit
9f9065ddb7
3
.gitignore
vendored
3
.gitignore
vendored
@ -4,3 +4,6 @@
|
||||
index.html
|
||||
package-lock.json
|
||||
yarn-error.log
|
||||
|
||||
# Perf related files
|
||||
isolate*.log
|
||||
2
perf/.gitignore
vendored
Normal file
2
perf/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
output*.css
|
||||
v8.json
|
||||
61
perf/fixture.css
Normal file
61
perf/fixture.css
Normal 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
15
perf/script.sh
Executable 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
28
perf/tailwind.config.js
Normal 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: [],
|
||||
}
|
||||
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
@ -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'),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
16
src/util/useMemo.js
Normal 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
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user