Skip to content

Commit

Permalink
Re-use existing entries in the rule cache (#9208)
Browse files Browse the repository at this point in the history
* Add test

* Reuse rule cache entries when possible

* Update changelog
  • Loading branch information
thecrypticace committed Aug 29, 2022
1 parent da85042 commit 58cc7ed
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 40 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Remove invalid `outline-hidden` utility ([#9147](https://github.com/tailwindlabs/tailwindcss/pull/9147))
- Honor the `hidden` attribute on elements in preflight ([#9174](https://github.com/tailwindlabs/tailwindcss/pull/9174))
- Don't stop watching atomically renamed files ([#9173](https://github.com/tailwindlabs/tailwindcss/pull/9173))
- Re-use existing entries in the rule cache ([#9208](https://github.com/tailwindlabs/tailwindcss/pull/9208))
- Don't output duplicate utilities ([#9208](https://github.com/tailwindlabs/tailwindcss/pull/9208))

## [3.1.8] - 2022-08-05

Expand Down
6 changes: 1 addition & 5 deletions src/lib/expandTailwindAtRules.js
Expand Up @@ -177,16 +177,12 @@ export default function expandTailwindAtRules(context) {
let classCacheCount = context.classCache.size

env.DEBUG && console.time('Generate rules')
let rules = generateRules(candidates, context)
generateRules(candidates, context)
env.DEBUG && console.timeEnd('Generate rules')

// We only ever add to the classCache, so if it didn't grow, there is nothing new.
env.DEBUG && console.time('Build stylesheet')
if (context.stylesheetCache === null || context.classCache.size !== classCacheCount) {
for (let rule of rules) {
context.ruleCache.add(rule)
}

context.stylesheetCache = buildStylesheet([...context.ruleCache], context)
}
env.DEBUG && console.timeEnd('Build stylesheet')
Expand Down
81 changes: 47 additions & 34 deletions src/lib/generateRules.js
Expand Up @@ -649,16 +649,49 @@ function inKeyframes(rule) {
return rule.parent && rule.parent.type === 'atrule' && rule.parent.name === 'keyframes'
}

function getImportantStrategy(important) {
if (important === true) {
return (rule) => {
if (inKeyframes(rule)) {
return
}

rule.walkDecls((d) => {
if (d.parent.type === 'rule' && !inKeyframes(d.parent)) {
d.important = true
}
})
}
}

if (typeof important === 'string') {
return (rule) => {
if (inKeyframes(rule)) {
return
}

rule.selectors = rule.selectors.map((selector) => {
return `${important} ${selector}`
})
}
}
}

function generateRules(candidates, context) {
let allRules = []
let strategy = getImportantStrategy(context.tailwindConfig.important)

for (let candidate of candidates) {
if (context.notClassCache.has(candidate)) {
continue
}

if (context.classCache.has(candidate)) {
allRules.push(context.classCache.get(candidate))
continue
}

if (context.candidateRuleCache.has(candidate)) {
allRules = allRules.concat(Array.from(context.candidateRuleCache.get(candidate)))
continue
}

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

context.classCache.set(candidate, matches)
allRules.push(matches)
}

// Strategy based on `tailwindConfig.important`
let strategy = ((important) => {
if (important === true) {
return (rule) => {
rule.walkDecls((d) => {
if (d.parent.type === 'rule' && !inKeyframes(d.parent)) {
d.important = true
}
})
}
}
let rules = context.candidateRuleCache.get(candidate) ?? new Set()
context.candidateRuleCache.set(candidate, rules)

if (typeof important === 'string') {
return (rule) => {
rule.selectors = rule.selectors.map((selector) => {
return `${important} ${selector}`
})
}
}
})(context.tailwindConfig.important)
for (const match of matches) {
let [{ sort, layer, options }, rule] = match

return allRules.flat(1).map(([{ sort, layer, options }, rule]) => {
if (options.respectImportant) {
if (strategy) {
if (options.respectImportant && strategy) {
let container = postcss.root({ nodes: [rule.clone()] })
container.walkRules((r) => {
if (inKeyframes(r)) {
return
}

strategy(r)
})
container.walkRules(strategy)
rule = container.nodes[0]
}

let newEntry = [sort | context.layerOrder[layer], rule]
rules.add(newEntry)
context.ruleCache.add(newEntry)
allRules.push(newEntry)
}
}

return [sort | context.layerOrder[layer], rule]
})
return allRules
}

function isArbitraryValue(input) {
Expand Down
1 change: 1 addition & 0 deletions src/lib/setupContextUtils.js
Expand Up @@ -873,6 +873,7 @@ export function createContext(tailwindConfig, changedContent = [], root = postcs
let context = {
disposables: [],
ruleCache: new Set(),
candidateRuleCache: new Map(),
classCache: new Map(),
applyClassCache: new Map(),
notClassCache: new Set(),
Expand Down
74 changes: 73 additions & 1 deletion tests/important-boolean.test.js
@@ -1,7 +1,8 @@
import fs from 'fs'
import path from 'path'
import * as sharedState from '../src/lib/sharedState'

import { run, css } from './util/run'
import { run, css, html } from './util/run'

test('important boolean', () => {
let config = {
Expand Down Expand Up @@ -63,3 +64,74 @@ test('important boolean', () => {
expect(result.css).toMatchFormattedCss(expected)
})
})

// This is in a describe block so we can use `afterEach` :)
describe('duplicate elision', () => {
let filePath = path.resolve(__dirname, './important-boolean-duplicates.test.html')

afterEach(async () => await fs.promises.unlink(filePath))

test('important rules are not duplicated when rebuilding', async () => {
let config = {
important: true,
content: [filePath],
}

await fs.promises.writeFile(
config.content[0],
html`
<div class="ml-2"></div>
<div class="ml-4"></div>
`
)

let input = css`
@tailwind utilities;
`

let result = await run(input, config)
let allContexts = Array.from(sharedState.contextMap.values())

let context = allContexts[allContexts.length - 1]

let ruleCacheSize1 = context.ruleCache.size

expect(result.css).toMatchFormattedCss(css`
.ml-2 {
margin-left: 0.5rem !important;
}
.ml-4 {
margin-left: 1rem !important;
}
`)

await fs.promises.writeFile(
config.content[0],
html`
<div class="ml-2"></div>
<div class="ml-6"></div>
`
)

result = await run(input, config)

let ruleCacheSize2 = context.ruleCache.size

expect(result.css).toMatchFormattedCss(css`
.ml-2 {
margin-left: 0.5rem !important;
}
.ml-4 {
margin-left: 1rem !important;
}
.ml-6 {
margin-left: 1.5rem !important;
}
`)

// The rule cache was effectively doubling in size previously
// because the rule cache was never de-duped
// This ensures this behavior doesn't return
expect(ruleCacheSize2 - ruleCacheSize1).toBeLessThan(10)
})
})

0 comments on commit 58cc7ed

Please sign in to comment.