Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance improvements #2171

Merged
merged 17 commits into from Aug 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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