Skip to content

Commit 58cc7ed

Browse files
Re-use existing entries in the rule cache (#9208)
* Add test * Reuse rule cache entries when possible * Update changelog
1 parent da85042 commit 58cc7ed

File tree

5 files changed

+124
-40
lines changed

5 files changed

+124
-40
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222
- Remove invalid `outline-hidden` utility ([#9147](https://github.com/tailwindlabs/tailwindcss/pull/9147))
2323
- Honor the `hidden` attribute on elements in preflight ([#9174](https://github.com/tailwindlabs/tailwindcss/pull/9174))
2424
- Don't stop watching atomically renamed files ([#9173](https://github.com/tailwindlabs/tailwindcss/pull/9173))
25+
- Re-use existing entries in the rule cache ([#9208](https://github.com/tailwindlabs/tailwindcss/pull/9208))
26+
- Don't output duplicate utilities ([#9208](https://github.com/tailwindlabs/tailwindcss/pull/9208))
2527

2628
## [3.1.8] - 2022-08-05
2729

src/lib/expandTailwindAtRules.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -177,16 +177,12 @@ export default function expandTailwindAtRules(context) {
177177
let classCacheCount = context.classCache.size
178178

179179
env.DEBUG && console.time('Generate rules')
180-
let rules = generateRules(candidates, context)
180+
generateRules(candidates, context)
181181
env.DEBUG && console.timeEnd('Generate rules')
182182

183183
// We only ever add to the classCache, so if it didn't grow, there is nothing new.
184184
env.DEBUG && console.time('Build stylesheet')
185185
if (context.stylesheetCache === null || context.classCache.size !== classCacheCount) {
186-
for (let rule of rules) {
187-
context.ruleCache.add(rule)
188-
}
189-
190186
context.stylesheetCache = buildStylesheet([...context.ruleCache], context)
191187
}
192188
env.DEBUG && console.timeEnd('Build stylesheet')

src/lib/generateRules.js

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -649,16 +649,49 @@ function inKeyframes(rule) {
649649
return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes'
650650
}
651651

652+
function getImportantStrategy(important) {
653+
if (important === true) {
654+
return (rule) => {
655+
if (inKeyframes(rule)) {
656+
return
657+
}
658+
659+
rule.walkDecls((d) => {
660+
if (d.parent.type === 'rule' && !inKeyframes(d.parent)) {
661+
d.important = true
662+
}
663+
})
664+
}
665+
}
666+
667+
if (typeof important === 'string') {
668+
return (rule) => {
669+
if (inKeyframes(rule)) {
670+
return
671+
}
672+
673+
rule.selectors = rule.selectors.map((selector) => {
674+
return `${important} ${selector}`
675+
})
676+
}
677+
}
678+
}
679+
652680
function generateRules(candidates, context) {
653681
let allRules = []
682+
let strategy = getImportantStrategy(context.tailwindConfig.important)
654683

655684
for (let candidate of candidates) {
656685
if (context.notClassCache.has(candidate)) {
657686
continue
658687
}
659688

660689
if (context.classCache.has(candidate)) {
661-
allRules.push(context.classCache.get(candidate))
690+
continue
691+
}
692+
693+
if (context.candidateRuleCache.has(candidate)) {
694+
allRules = allRules.concat(Array.from(context.candidateRuleCache.get(candidate)))
662695
continue
663696
}
664697

@@ -670,47 +703,27 @@ function generateRules(candidates, context) {
670703
}
671704

672705
context.classCache.set(candidate, matches)
673-
allRules.push(matches)
674-
}
675706

676-
// Strategy based on `tailwindConfig.important`
677-
let strategy = ((important) => {
678-
if (important === true) {
679-
return (rule) => {
680-
rule.walkDecls((d) => {
681-
if (d.parent.type === 'rule' && !inKeyframes(d.parent)) {
682-
d.important = true
683-
}
684-
})
685-
}
686-
}
707+
let rules = context.candidateRuleCache.get(candidate) ?? new Set()
708+
context.candidateRuleCache.set(candidate, rules)
687709

688-
if (typeof important === 'string') {
689-
return (rule) => {
690-
rule.selectors = rule.selectors.map((selector) => {
691-
return `${important} ${selector}`
692-
})
693-
}
694-
}
695-
})(context.tailwindConfig.important)
710+
for (const match of matches) {
711+
let [{ sort, layer, options }, rule] = match
696712

697-
return allRules.flat(1).map(([{ sort, layer, options }, rule]) => {
698-
if (options.respectImportant) {
699-
if (strategy) {
713+
if (options.respectImportant && strategy) {
700714
let container = postcss.root({ nodes: [rule.clone()] })
701-
container.walkRules((r) => {
702-
if (inKeyframes(r)) {
703-
return
704-
}
705-
706-
strategy(r)
707-
})
715+
container.walkRules(strategy)
708716
rule = container.nodes[0]
709717
}
718+
719+
let newEntry = [sort | context.layerOrder[layer], rule]
720+
rules.add(newEntry)
721+
context.ruleCache.add(newEntry)
722+
allRules.push(newEntry)
710723
}
724+
}
711725

712-
return [sort | context.layerOrder[layer], rule]
713-
})
726+
return allRules
714727
}
715728

716729
function isArbitraryValue(input) {

src/lib/setupContextUtils.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,7 @@ export function createContext(tailwindConfig, changedContent = [], root = postcs
873873
let context = {
874874
disposables: [],
875875
ruleCache: new Set(),
876+
candidateRuleCache: new Map(),
876877
classCache: new Map(),
877878
applyClassCache: new Map(),
878879
notClassCache: new Set(),

tests/important-boolean.test.js

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import fs from 'fs'
22
import path from 'path'
3+
import * as sharedState from '../src/lib/sharedState'
34

4-
import { run, css } from './util/run'
5+
import { run, css, html } from './util/run'
56

67
test('important boolean', () => {
78
let config = {
@@ -63,3 +64,74 @@ test('important boolean', () => {
6364
expect(result.css).toMatchFormattedCss(expected)
6465
})
6566
})
67+
68+
// This is in a describe block so we can use `afterEach` :)
69+
describe('duplicate elision', () => {
70+
let filePath = path.resolve(__dirname, './important-boolean-duplicates.test.html')
71+
72+
afterEach(async () => await fs.promises.unlink(filePath))
73+
74+
test('important rules are not duplicated when rebuilding', async () => {
75+
let config = {
76+
important: true,
77+
content: [filePath],
78+
}
79+
80+
await fs.promises.writeFile(
81+
config.content[0],
82+
html`
83+
<div class="ml-2"></div>
84+
<div class="ml-4"></div>
85+
`
86+
)
87+
88+
let input = css`
89+
@tailwind utilities;
90+
`
91+
92+
let result = await run(input, config)
93+
let allContexts = Array.from(sharedState.contextMap.values())
94+
95+
let context = allContexts[allContexts.length - 1]
96+
97+
let ruleCacheSize1 = context.ruleCache.size
98+
99+
expect(result.css).toMatchFormattedCss(css`
100+
.ml-2 {
101+
margin-left: 0.5rem !important;
102+
}
103+
.ml-4 {
104+
margin-left: 1rem !important;
105+
}
106+
`)
107+
108+
await fs.promises.writeFile(
109+
config.content[0],
110+
html`
111+
<div class="ml-2"></div>
112+
<div class="ml-6"></div>
113+
`
114+
)
115+
116+
result = await run(input, config)
117+
118+
let ruleCacheSize2 = context.ruleCache.size
119+
120+
expect(result.css).toMatchFormattedCss(css`
121+
.ml-2 {
122+
margin-left: 0.5rem !important;
123+
}
124+
.ml-4 {
125+
margin-left: 1rem !important;
126+
}
127+
.ml-6 {
128+
margin-left: 1.5rem !important;
129+
}
130+
`)
131+
132+
// The rule cache was effectively doubling in size previously
133+
// because the rule cache was never de-duped
134+
// This ensures this behavior doesn't return
135+
expect(ruleCacheSize2 - ruleCacheSize1).toBeLessThan(10)
136+
})
137+
})

0 commit comments

Comments
 (0)