Skip to content

Commit eac11cf

Browse files
authored
Performance improvements + memory leak fix (#3032)
* fix memory leak * add optional condition to hasAtRule * use known tree to handle `@apply` when required `@tailwind` at rules exists Otherwise we will generate the lookup tree. * only generate the missing `@tailwind` atrules when using `@apply` * update perf config to reflect 2.0 changes * update changelog * ensure lookup tree is correctly cached based on used tailwind atrules
1 parent f12458a commit eac11cf

File tree

7 files changed

+125
-40
lines changed

7 files changed

+125
-40
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Fix issue with `@apply` not working as expected with `!important` inside an atrule ([#2824](https://github.com/tailwindlabs/tailwindcss/pull/2824))
1313
- Fix issue with `@apply` not working as expected with defined classes ([#2832](https://github.com/tailwindlabs/tailwindcss/pull/2832))
14+
- Fix memory leak, and broken `@apply` when splitting up files ([#3032](https://github.com/tailwindlabs/tailwindcss/pull/3032))
1415

1516
### Added
1617

__tests__/applyAtRule.test.js

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -337,16 +337,17 @@ test('you can apply utility classes that do not actually exist as long as they w
337337
})
338338
})
339339

340-
test('the shadow lookup is only used if no @tailwind rules were in the source tree', () => {
340+
test('shadow lookup will be constructed when we have missing @tailwind atrules', () => {
341341
const input = `
342342
@tailwind base;
343+
343344
.foo { @apply mt-4; }
344345
`
345346

346347
expect.assertions(1)
347348

348-
return run(input).catch((e) => {
349-
expect(e).toMatchObject({ name: 'CssSyntaxError' })
349+
return run(input).then((result) => {
350+
expect(result.css).toContain(`.foo { margin-top: 1rem;\n}`)
350351
})
351352
})
352353

@@ -1362,3 +1363,40 @@ test('declarations within a rule that uses @apply with !important remain not !im
13621363
expect(result.warnings().length).toBe(0)
13631364
})
13641365
})
1366+
1367+
test('lookup tree is correctly cached based on used tailwind atrules', async () => {
1368+
const input1 = `
1369+
@tailwind utilities;
1370+
1371+
.foo { @apply mt-4; }
1372+
`
1373+
1374+
const input2 = `
1375+
@tailwind components;
1376+
1377+
.foo { @apply mt-4; }
1378+
`
1379+
1380+
let config = {
1381+
corePlugins: [],
1382+
plugins: [
1383+
function ({ addUtilities, addComponents }) {
1384+
addUtilities({ '.mt-4': { marginTop: '1rem' } }, [])
1385+
addComponents({ '.container': { maxWidth: '500px' } }, [])
1386+
},
1387+
],
1388+
}
1389+
1390+
let output1 = await run(input1, config)
1391+
let output2 = await run(input2, config)
1392+
1393+
expect(output1.css).toMatchCss(`
1394+
.mt-4 { margin-top: 1rem; }
1395+
.foo { margin-top: 1rem; }
1396+
`)
1397+
1398+
expect(output2.css).toMatchCss(`
1399+
.container { max-width: 500px; }
1400+
.foo { margin-top: 1rem; }
1401+
`)
1402+
})

perf/tailwind.config.js

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1+
let colors = require('../colors')
12
module.exports = {
2-
future: 'all',
3-
experimental: 'all',
43
purge: [],
4+
darkMode: 'class',
55
theme: {
6-
extend: {},
6+
extend: { colors },
77
},
88
variants: [
99
'responsive',
10-
'motion-safe',
11-
'motion-reduce',
1210
'group-hover',
1311
'group-focus',
1412
'hover',
@@ -19,10 +17,6 @@ module.exports = {
1917
'visited',
2018
'disabled',
2119
'checked',
22-
'first',
23-
'last',
24-
'odd',
25-
'even',
2620
],
2721
plugins: [],
2822
}

src/lib/substituteClassApplyAtRules.js

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,25 @@ import substituteScreenAtRules from './substituteScreenAtRules'
1111
import prefixSelector from '../util/prefixSelector'
1212
import { useMemo } from '../util/useMemo'
1313

14-
function hasAtRule(css, atRule) {
15-
let foundAtRule = false
16-
17-
css.walkAtRules(atRule, () => {
18-
foundAtRule = true
19-
return false
20-
})
14+
function hasAtRule(css, atRule, condition) {
15+
let found = false
16+
17+
css.walkAtRules(
18+
atRule,
19+
condition === undefined
20+
? () => {
21+
found = true
22+
return false
23+
}
24+
: (node) => {
25+
if (condition(node)) {
26+
found = true
27+
return false
28+
}
29+
}
30+
)
2131

22-
return foundAtRule
32+
return found
2333
}
2434

2535
function cloneWithoutChildren(node) {
@@ -298,7 +308,7 @@ function processApplyAtRules(css, lookupTree, config) {
298308
return css
299309
}
300310

301-
let defaultTailwindTree = null
311+
let defaultTailwindTree = new Map()
302312

303313
export default function substituteClassApplyAtRules(config, getProcessedPlugins, configChanged) {
304314
return function (css) {
@@ -307,15 +317,29 @@ export default function substituteClassApplyAtRules(config, getProcessedPlugins,
307317
return css
308318
}
309319

310-
// Tree already contains @tailwind rules, don't prepend default Tailwind tree
311-
if (hasAtRule(css, 'tailwind')) {
320+
let requiredTailwindAtRules = ['base', 'components', 'utilities']
321+
if (
322+
hasAtRule(css, 'tailwind', (node) => {
323+
let idx = requiredTailwindAtRules.indexOf(node.params)
324+
if (idx !== -1) requiredTailwindAtRules.splice(idx, 1)
325+
if (requiredTailwindAtRules.length <= 0) return true
326+
return false
327+
})
328+
) {
329+
// Tree already contains all the at rules (requiredTailwindAtRules)
312330
return processApplyAtRules(css, postcss.root(), config)
313331
}
314332

315-
// Tree contains no @tailwind rules, so generate all of Tailwind's styles and
316-
// prepend them to the user's CSS. Important for <style> blocks in Vue components.
333+
let lookupKey = requiredTailwindAtRules.join(',')
334+
335+
// We mutated the `requiredTailwindAtRules`, but when we hit this point in
336+
// time, it means that we don't have all the atrules. The missing atrules
337+
// are listed inside the requiredTailwindAtRules, which we can use to fill
338+
// in the missing pieces.
339+
//
340+
// Important for <style> blocks in Vue components.
317341
const generateLookupTree =
318-
configChanged || defaultTailwindTree === null
342+
configChanged || !defaultTailwindTree.has(lookupKey)
319343
? () => {
320344
return postcss([
321345
substituteTailwindAtRules(config, getProcessedPlugins()),
@@ -325,20 +349,15 @@ export default function substituteClassApplyAtRules(config, getProcessedPlugins,
325349
convertLayerAtRulesToControlComments(config),
326350
substituteScreenAtRules(config),
327351
])
328-
.process(
329-
`
330-
@tailwind base;
331-
@tailwind components;
332-
@tailwind utilities;
333-
`,
334-
{ from: undefined }
335-
)
352+
.process(requiredTailwindAtRules.map((rule) => `@tailwind ${rule};`).join('\n'), {
353+
from: undefined,
354+
})
336355
.then((result) => {
337-
defaultTailwindTree = result
338-
return defaultTailwindTree
356+
defaultTailwindTree.set(lookupKey, result)
357+
return result
339358
})
340359
}
341-
: () => Promise.resolve(defaultTailwindTree)
360+
: () => Promise.resolve(defaultTailwindTree.get(lookupKey))
342361

343362
return generateLookupTree().then((result) => {
344363
return processApplyAtRules(css, result.root, config)

src/processTailwindFeatures.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { issueFlagNotices } from './featureFlags.js'
1818

1919
import hash from 'object-hash'
2020
import log from './util/log'
21+
import { shared } from './util/disposables'
2122

2223
let previousConfig = null
2324
let processedPlugins = null
@@ -30,6 +31,7 @@ export default function (getConfig) {
3031
previousConfig = config
3132

3233
if (configChanged) {
34+
shared.dispose()
3335
if (config.target) {
3436
log.warn([
3537
'The `target` feature has been removed in Tailwind CSS v2.0.',

src/util/disposables.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export function disposables() {
2+
let disposables = []
3+
4+
let api = {
5+
add(cb) {
6+
disposables.push(cb)
7+
8+
return () => {
9+
let idx = disposables.indexOf(cb)
10+
if (idx !== -1) disposables.splice(idx, 1)
11+
}
12+
},
13+
dispose() {
14+
disposables.splice(0).forEach((dispose) => dispose())
15+
},
16+
}
17+
18+
return api
19+
}
20+
21+
// A shared disposables collection
22+
export let shared = disposables()

src/util/useMemo.js

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
1+
import { shared } from './disposables'
2+
13
export function useMemo(cb, keyResolver) {
2-
const cache = new Map()
4+
let cache = new Map()
5+
6+
function clearCache() {
7+
cache.clear()
8+
shared.add(clearCache)
9+
}
10+
11+
shared.add(clearCache)
312

413
return (...args) => {
5-
const key = keyResolver(...args)
14+
let key = keyResolver(...args)
615

716
if (cache.has(key)) {
817
return cache.get(key)
918
}
1019

11-
const result = cb(...args)
20+
let result = cb(...args)
1221
cache.set(key, result)
1322

1423
return result

0 commit comments

Comments
 (0)