Skip to content

Commit 9f9065d

Browse files
authored
Merge pull request #2171 from tailwindlabs/perf-improvements
Performance improvements
2 parents 053ab65 + 33ee646 commit 9f9065d

File tree

10 files changed

+268
-100
lines changed

10 files changed

+268
-100
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
index.html
55
package-lock.json
66
yarn-error.log
7+
8+
# Perf related files
9+
isolate*.log

perf/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
output*.css
2+
v8.json

perf/fixture.css

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
@tailwind base;
2+
3+
@tailwind components;
4+
5+
@tailwind utilities;
6+
7+
8+
.btn-1-xl {
9+
@apply sm:space-x-0;
10+
@apply xl:space-x-0;
11+
@apply sm:space-x-1;
12+
@apply xl:space-x-1;
13+
@apply sm:space-y-0;
14+
@apply xl:space-y-0;
15+
@apply sm:space-y-1;
16+
@apply xl:space-y-1;
17+
}
18+
.btn-2-xl {
19+
@apply sm:space-x-0;
20+
@apply xl:space-x-0;
21+
@apply sm:space-x-1;
22+
@apply xl:space-x-1;
23+
@apply sm:space-y-0;
24+
@apply xl:space-y-0;
25+
@apply sm:space-y-1;
26+
@apply xl:space-y-1;
27+
@apply btn-1-xl;
28+
}
29+
.btn-3-xl {
30+
@apply sm:space-x-0;
31+
@apply xl:space-x-0;
32+
@apply sm:space-x-1;
33+
@apply xl:space-x-1;
34+
@apply sm:space-y-0;
35+
@apply xl:space-y-0;
36+
@apply sm:space-y-1;
37+
@apply xl:space-y-1;
38+
@apply btn-2-xl;
39+
}
40+
.btn-4-xl {
41+
@apply sm:space-x-0;
42+
@apply xl:space-x-0;
43+
@apply sm:space-x-1;
44+
@apply xl:space-x-1;
45+
@apply sm:space-y-0;
46+
@apply xl:space-y-0;
47+
@apply sm:space-y-1;
48+
@apply xl:space-y-1;
49+
@apply btn-3-xl;
50+
}
51+
.btn-5-xl {
52+
@apply sm:space-x-0;
53+
@apply xl:space-x-0;
54+
@apply sm:space-x-1;
55+
@apply xl:space-x-1;
56+
@apply sm:space-y-0;
57+
@apply xl:space-y-0;
58+
@apply sm:space-y-1;
59+
@apply xl:space-y-1;
60+
@apply btn-4-xl;
61+
}

perf/script.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Cleanup existing perf stuff
2+
rm isolate-*.log
3+
4+
# Ensure we use the latest build version
5+
npm run babelify
6+
7+
# Run Tailwind on the big fixture file & profile it
8+
node --prof lib/cli.js build ./perf/fixture.css -c ./perf/tailwind.config.js -o ./perf/output.css
9+
10+
# Generate flame graph
11+
node --prof-process --preprocess -j isolate*.log > ./perf/v8.json
12+
13+
# Now visit: https://mapbox.github.io/flamebearer/
14+
# And drag that v8.json file in there!
15+
# You can put "./lib" in the search box which will highlight all our code in green.

perf/tailwind.config.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module.exports = {
2+
future: 'all',
3+
experimental: 'all',
4+
purge: [],
5+
theme: {
6+
extend: {},
7+
},
8+
variants: [
9+
'responsive',
10+
'motion-safe',
11+
'motion-reduce',
12+
'group-hover',
13+
'group-focus',
14+
'hover',
15+
'focus-within',
16+
'focus-visible',
17+
'focus',
18+
'active',
19+
'visited',
20+
'disabled',
21+
'checked',
22+
'first',
23+
'last',
24+
'odd',
25+
'even',
26+
],
27+
plugins: [],
28+
}

src/flagged/applyComplexClasses.js

Lines changed: 74 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import substituteResponsiveAtRules from '../lib/substituteResponsiveAtRules'
88
import convertLayerAtRulesToControlComments from '../lib/convertLayerAtRulesToControlComments'
99
import substituteScreenAtRules from '../lib/substituteScreenAtRules'
1010
import prefixSelector from '../util/prefixSelector'
11+
import { useMemo } from '../util/useMemo'
1112

1213
function hasAtRule(css, atRule) {
1314
let foundAtRule = false
@@ -20,35 +21,48 @@ function hasAtRule(css, atRule) {
2021
return foundAtRule
2122
}
2223

24+
function cloneWithoutChildren(node) {
25+
if (node.type === 'atrule') {
26+
return postcss.atRule({ name: node.name, params: node.params })
27+
}
28+
29+
if (node.type === 'rule') {
30+
return postcss.rule({ name: node.name, selectors: node.selectors })
31+
}
32+
33+
const clone = node.clone()
34+
clone.removeAll()
35+
return clone
36+
}
37+
38+
const tailwindApplyPlaceholder = selectorParser.attribute({
39+
attribute: '__TAILWIND-APPLY-PLACEHOLDER__',
40+
})
41+
2342
function generateRulesFromApply({ rule, utilityName: className, classPosition }, replaceWith) {
24-
const processedSelectors = rule.selectors.map(selector => {
25-
const processor = selectorParser(selectors => {
26-
let i = 0
27-
selectors.walkClasses(c => {
28-
if (c.value === className && classPosition === i) {
29-
c.replaceWith(selectorParser.attribute({ attribute: '__TAILWIND-APPLY-PLACEHOLDER__' }))
30-
}
31-
i++
32-
})
43+
const parser = selectorParser(selectors => {
44+
let i = 0
45+
selectors.walkClasses(c => {
46+
if (classPosition === i++ && c.value === className) {
47+
c.replaceWith(tailwindApplyPlaceholder)
48+
}
3349
})
50+
})
3451

52+
const processedSelectors = rule.selectors.map(selector => {
3553
// You could argue we should make this replacement at the AST level, but if we believe
3654
// the placeholder string is safe from collisions then it is safe to do this is a simple
3755
// string replacement, and much, much faster.
38-
const processedSelector = processor
39-
.processSync(selector)
40-
.replace('[__TAILWIND-APPLY-PLACEHOLDER__]', replaceWith)
41-
42-
return processedSelector
56+
return parser.processSync(selector).replace('[__TAILWIND-APPLY-PLACEHOLDER__]', replaceWith)
4357
})
4458

4559
const cloned = rule.clone()
4660
let current = cloned
4761
let parent = rule.parent
4862

4963
while (parent && parent.type !== 'root') {
50-
const parentClone = parent.clone()
51-
parentClone.removeAll()
64+
const parentClone = cloneWithoutChildren(parent)
65+
5266
parentClone.append(current)
5367
current.parent = parentClone
5468
current = parentClone
@@ -59,19 +73,21 @@ function generateRulesFromApply({ rule, utilityName: className, classPosition },
5973
return current
6074
}
6175

62-
function extractUtilityNames(selector) {
63-
const processor = selectorParser(selectors => {
64-
let classes = []
76+
const extractUtilityNamesParser = selectorParser(selectors => {
77+
let classes = []
78+
selectors.walkClasses(c => classes.push(c.value))
79+
return classes
80+
})
6581

66-
selectors.walkClasses(c => {
67-
classes.push(c)
68-
})
69-
70-
return classes.map(c => c.value)
71-
})
82+
const extractUtilityNames = useMemo(
83+
selector => extractUtilityNamesParser.transformSync(selector),
84+
selector => selector
85+
)
7286

73-
return processor.transformSync(selector)
74-
}
87+
const cloneRuleWithParent = useMemo(
88+
rule => rule.clone({ parent: rule.parent }),
89+
rule => rule
90+
)
7591

7692
function buildUtilityMap(css) {
7793
let index = 0
@@ -89,8 +105,9 @@ function buildUtilityMap(css) {
89105
index,
90106
utilityName,
91107
classPosition: i,
92-
rule: rule.clone({ parent: rule.parent }),
93-
containsApply: hasAtRule(rule, 'apply'),
108+
get rule() {
109+
return cloneRuleWithParent(rule)
110+
},
94111
})
95112
index++
96113
})
@@ -136,15 +153,11 @@ function mergeAdjacentRules(initialRule, rulesToInsert) {
136153

137154
function makeExtractUtilityRules(css, config) {
138155
const utilityMap = buildUtilityMap(css)
139-
const orderUtilityMap = _.fromPairs(
140-
_.flatMap(_.toPairs(utilityMap), ([_utilityName, utilities]) => {
141-
return utilities.map(utility => {
142-
return [utility.index, utility]
143-
})
144-
})
145-
)
146-
return function(utilityNames, rule) {
147-
return _.flatMap(utilityNames, utilityName => {
156+
157+
return function extractUtilityRules(utilityNames, rule) {
158+
const combined = []
159+
160+
utilityNames.forEach(utilityName => {
148161
if (utilityMap[utilityName] === undefined) {
149162
// Look for prefixed utility in case the user has goofed
150163
const prefixedUtility = prefixSelector(config.prefix, `.${utilityName}`).slice(1)
@@ -160,17 +173,18 @@ function makeExtractUtilityRules(css, config) {
160173
{ word: utilityName }
161174
)
162175
}
163-
return utilityMap[utilityName].map(({ index }) => index)
176+
177+
combined.push(...utilityMap[utilityName])
164178
})
165-
.sort((a, b) => a - b)
166-
.map(i => orderUtilityMap[i])
179+
180+
return combined.sort((a, b) => a.index - b.index)
167181
}
168182
}
169183

170184
function processApplyAtRules(css, lookupTree, config) {
171185
const extractUtilityRules = makeExtractUtilityRules(lookupTree, config)
172186

173-
while (hasAtRule(css, 'apply')) {
187+
do {
174188
css.walkRules(rule => {
175189
const applyRules = []
176190

@@ -229,13 +243,29 @@ function processApplyAtRules(css, lookupTree, config) {
229243
rule.remove()
230244
}
231245
})
232-
}
246+
247+
// We already know that we have at least 1 @apply rule. Otherwise this
248+
// function would not have been called. Therefore we can execute this code
249+
// at least once. This also means that in the best case scenario we only
250+
// call this 2 times, instead of 3 times.
251+
// 1st time -> before we call this function
252+
// 2nd time -> when we check if we have to do this loop again (because do {} while (check))
253+
// .. instead of
254+
// 1st time -> before we call this function
255+
// 2nd time -> when we check the first time (because while (check) do {})
256+
// 3rd time -> when we re-check to see if we should do this loop again
257+
} while (hasAtRule(css, 'apply'))
233258

234259
return css
235260
}
236261

237262
export default function applyComplexClasses(config, getProcessedPlugins) {
238263
return function(css) {
264+
// We can stop already when we don't have any @apply rules. Vue users: you're welcome!
265+
if (!hasAtRule(css, 'apply')) {
266+
return css
267+
}
268+
239269
// Tree already contains @tailwind rules, don't prepend default Tailwind tree
240270
if (hasAtRule(css, 'tailwind')) {
241271
return processApplyAtRules(css, css, config)
@@ -261,7 +291,7 @@ export default function applyComplexClasses(config, getProcessedPlugins) {
261291
)
262292
.then(result => {
263293
// Prepend Tailwind's generated classes to the tree so they are available for `@apply`
264-
const lookupTree = _.tap(css.clone(), tree => tree.prepend(result.root))
294+
const lookupTree = _.tap(result.root, tree => tree.append(css.clone()))
265295
return processApplyAtRules(css, lookupTree, config)
266296
})
267297
}

0 commit comments

Comments
 (0)