Skip to content

Commit

Permalink
Merge pull request #2171 from tailwindlabs/perf-improvements
Browse files Browse the repository at this point in the history
Performance improvements
  • Loading branch information
adamwathan committed Aug 18, 2020
2 parents 053ab65 + 33ee646 commit 9f9065d
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 100 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Expand Up @@ -4,3 +4,6 @@
index.html
package-lock.json
yarn-error.log

# Perf related files
isolate*.log
2 changes: 2 additions & 0 deletions perf/.gitignore
@@ -0,0 +1,2 @@
output*.css
v8.json
61 changes: 61 additions & 0 deletions 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;
}
15 changes: 15 additions & 0 deletions 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.
28 changes: 28 additions & 0 deletions 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: [],
}
118 changes: 74 additions & 44 deletions src/flagged/applyComplexClasses.js
Expand Up @@ -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
Expand All @@ -20,35 +21,48 @@ 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__',
})

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++
})
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()
let current = cloned
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
Expand All @@ -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)
})

return classes.map(c => c.value)
})
const extractUtilityNames = useMemo(
selector => extractUtilityNamesParser.transformSync(selector),
selector => selector
)

return processor.transformSync(selector)
}
const cloneRuleWithParent = useMemo(
rule => rule.clone({ parent: rule.parent }),
rule => rule
)

function buildUtilityMap(css) {
let index = 0
Expand All @@ -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++
})
Expand Down Expand Up @@ -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)
Expand All @@ -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 = []

Expand Down Expand Up @@ -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)
Expand All @@ -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)
})
}
Expand Down

0 comments on commit 9f9065d

Please sign in to comment.