Skip to content

Commit 99b73ee

Browse files
Improve performance of @tailwindcss/postcss and @tailwindcss/vite (#15226)
This PR improves the performance of the `@tailwindcss/postcss` and `@tailwindcss/vite` implementations. The issue is that in some scenarios, if you have multiple `.css` files, then all of the CSS files are ran through the Tailwind CSS compiler. The issue with this is that in a lot of cases, the CSS files aren't even related to Tailwind CSS at all. E.g.: in a Next.js project, if you use the `next/font/local` tool, then every font you used will be in a separate CSS file. This means that we run Tailwind CSS in all these files as well. That said, running Tailwind CSS on these files isn't the end of the world because we still need to handle `@import` in case `@tailwind utilities` is being used. However, we also run the auto source detection logic for every CSS file in the system. This part is bad. To solve this, this PR introduces an internal `features` to collect what CSS features are used throughout the system (`@import`, `@plugin`, `@apply`, `@tailwind utilities`, etc…) The `@tailwindcss/postcss` and `@tailwindcss/vite` plugin can use that information to decide if they can take some shortcuts or not. --- Overall, this means that we don't run the slow parts of Tailwind CSS if we don't need to. --------- Co-authored-by: Adam Wathan <[email protected]>
1 parent 6abd808 commit 99b73ee

File tree

22 files changed

+322
-162
lines changed

22 files changed

+322
-162
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10-
- Nothing yet!
10+
### Fixed
11+
12+
- Don't scan source files for utilities unless `@tailwind utilities` is present in the CSS in `@tailwindcss/postcss` and `@tailwindcss/vite` ([#15226](https://github.com/tailwindlabs/tailwindcss/pull/15226))
13+
- Skip reserializing CSS files that don't use Tailwind features in `@tailwindcss/postcss` and `@tailwindcss/vite` ([#15226](https://github.com/tailwindlabs/tailwindcss/pull/15226))
1114

1215
## [4.0.0-beta.3] - 2024-11-27
1316

packages/@tailwindcss-node/src/compile.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import { pathToFileURL } from 'node:url'
77
import {
88
__unstable__loadDesignSystem as ___unstable__loadDesignSystem,
99
compile as _compile,
10+
Features,
1011
} from 'tailwindcss'
1112
import { getModuleDependencies } from './get-module-dependencies'
1213
import { rewriteUrls } from './urls'
1314

15+
export { Features }
16+
1417
export type Resolver = (id: string, base: string) => Promise<string | false | undefined>
1518

1619
export async function compile(

packages/@tailwindcss-node/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as Module from 'node:module'
22
import { pathToFileURL } from 'node:url'
33
import * as env from './env'
4-
export { __unstable__loadDesignSystem, compile } from './compile'
4+
export { __unstable__loadDesignSystem, compile, Features } from './compile'
55
export * from './normalize-path'
66
export { env }
77

packages/@tailwindcss-postcss/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"devDependencies": {
4141
"@types/node": "catalog:",
4242
"@types/postcss-import": "14.0.3",
43+
"dedent": "1.5.3",
4344
"internal-example-plugin": "workspace:*",
4445
"postcss-import": "^16.1.0"
4546
}

packages/@tailwindcss-postcss/src/index.test.ts

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import dedent from 'dedent'
12
import { unlink, writeFile } from 'node:fs/promises'
23
import postcss from 'postcss'
34
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
@@ -9,16 +10,20 @@ import tailwindcss from './index'
910
// We place it in packages/ because Vitest runs in the monorepo root,
1011
// and packages/tailwindcss must be a sub-folder for
1112
// @import 'tailwindcss' to work.
12-
const INPUT_CSS_PATH = `${__dirname}/fixtures/example-project/input.css`
13+
function inputCssFilePath() {
14+
// Including the current test name to ensure that the cache is invalidated per
15+
// test otherwise the cache will be used across tests.
16+
return `${__dirname}/fixtures/example-project/input.css?test=${expect.getState().currentTestName}`
17+
}
1318

14-
const css = String.raw
19+
const css = dedent
1520

1621
test("`@import 'tailwindcss'` is replaced with the generated CSS", async () => {
1722
let processor = postcss([
1823
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
1924
])
2025

21-
let result = await processor.process(`@import 'tailwindcss'`, { from: INPUT_CSS_PATH })
26+
let result = await processor.process(`@import 'tailwindcss'`, { from: inputCssFilePath() })
2227

2328
expect(result.css.trim()).toMatchSnapshot()
2429

@@ -49,8 +54,6 @@ test('output is optimized by Lightning CSS', async () => {
4954
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
5055
])
5156

52-
// `@apply` is used because Lightning is skipped if neither `@tailwind` nor
53-
// `@apply` is used.
5457
let result = await processor.process(
5558
css`
5659
@layer utilities {
@@ -65,7 +68,7 @@ test('output is optimized by Lightning CSS', async () => {
6568
}
6669
}
6770
`,
68-
{ from: INPUT_CSS_PATH },
71+
{ from: inputCssFilePath() },
6972
)
7073

7174
expect(result.css.trim()).toMatchInlineSnapshot(`
@@ -86,16 +89,14 @@ test('@apply can be used without emitting the theme in the CSS file', async () =
8689
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
8790
])
8891

89-
// `@apply` is used because Lightning is skipped if neither `@tailwind` nor
90-
// `@apply` is used.
9192
let result = await processor.process(
9293
css`
9394
@import 'tailwindcss/theme.css' theme(reference);
9495
.foo {
9596
@apply text-red-500;
9697
}
9798
`,
98-
{ from: INPUT_CSS_PATH },
99+
{ from: inputCssFilePath() },
99100
)
100101

101102
expect(result.css.trim()).toMatchInlineSnapshot(`
@@ -116,7 +117,7 @@ describe('processing without specifying a base path', () => {
116117
test('the current working directory is used by default', async () => {
117118
let processor = postcss([tailwindcss({ optimize: { minify: false } })])
118119

119-
let result = await processor.process(`@import "tailwindcss"`, { from: INPUT_CSS_PATH })
120+
let result = await processor.process(`@import "tailwindcss"`, { from: inputCssFilePath() })
120121

121122
expect(result.css).toContain(
122123
".md\\:\\[\\&\\:hover\\]\\:content-\\[\\'testing_default_base_path\\'\\]",
@@ -142,7 +143,7 @@ describe('plugins', () => {
142143
@import 'tailwindcss/utilities';
143144
@plugin './plugin.js';
144145
`,
145-
{ from: INPUT_CSS_PATH },
146+
{ from: inputCssFilePath() },
146147
)
147148

148149
expect(result.css.trim()).toMatchInlineSnapshot(`
@@ -202,7 +203,7 @@ describe('plugins', () => {
202203
@import 'tailwindcss/utilities';
203204
@plugin 'internal-example-plugin';
204205
`,
205-
{ from: INPUT_CSS_PATH },
206+
{ from: inputCssFilePath() },
206207
)
207208

208209
expect(result.css.trim()).toMatchInlineSnapshot(`
@@ -222,3 +223,28 @@ describe('plugins', () => {
222223
`)
223224
})
224225
})
226+
227+
test('bail early when Tailwind is not used', async () => {
228+
let processor = postcss([
229+
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
230+
])
231+
232+
let result = await processor.process(
233+
css`
234+
.custom-css {
235+
color: red;
236+
}
237+
`,
238+
{ from: inputCssFilePath() },
239+
)
240+
241+
// `fixtures/example-project` includes an `underline` candidate. But since we
242+
// didn't use `@tailwind utilities` we didn't scan for utilities.
243+
expect(result.css).not.toContain('.underline {')
244+
245+
expect(result.css.trim()).toMatchInlineSnapshot(`
246+
".custom-css {
247+
color: red;
248+
}"
249+
`)
250+
})

packages/@tailwindcss-postcss/src/index.ts

Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import QuickLRU from '@alloc/quick-lru'
2-
import { compile, env } from '@tailwindcss/node'
2+
import { compile, env, Features } from '@tailwindcss/node'
33
import { clearRequireCache } from '@tailwindcss/node/require-cache'
44
import { Scanner } from '@tailwindcss/oxide'
5-
import { Features, transform } from 'lightningcss'
5+
import { Features as LightningCssFeatures, transform } from 'lightningcss'
66
import fs from 'node:fs'
77
import path from 'node:path'
88
import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss'
@@ -63,7 +63,9 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
6363

6464
async function createCompiler() {
6565
env.DEBUG && console.time('[@tailwindcss/postcss] Setup compiler')
66-
clearRequireCache(context.fullRebuildPaths)
66+
if (context.fullRebuildPaths.length > 0 && !isInitialBuild) {
67+
clearRequireCache(context.fullRebuildPaths)
68+
}
6769

6870
context.fullRebuildPaths = []
6971

@@ -86,6 +88,10 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
8688
// guarantee a `build()` function is available.
8789
context.compiler ??= await createCompiler()
8890

91+
if (context.compiler.features === Features.None) {
92+
return
93+
}
94+
8995
let rebuildStrategy: 'full' | 'incremental' = 'incremental'
9096

9197
// Track file modification times to CSS files
@@ -154,46 +160,49 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {
154160
}
155161

156162
env.DEBUG && console.time('[@tailwindcss/postcss] Scan for candidates')
157-
let candidates = context.scanner.scan()
163+
let candidates =
164+
context.compiler.features & Features.Utilities ? context.scanner.scan() : []
158165
env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Scan for candidates')
159166

160-
// Add all found files as direct dependencies
161-
for (let file of context.scanner.files) {
162-
result.messages.push({
163-
type: 'dependency',
164-
plugin: '@tailwindcss/postcss',
165-
file,
166-
parent: result.opts.from,
167-
})
168-
}
169-
170-
// Register dependencies so changes in `base` cause a rebuild while
171-
// giving tools like Vite or Parcel a glob that can be used to limit
172-
// the files that cause a rebuild to only those that match it.
173-
for (let { base: globBase, pattern } of context.scanner.globs) {
174-
// Avoid adding a dependency on the base directory itself, since it
175-
// causes Next.js to start an endless recursion if the `distDir` is
176-
// configured to anything other than the default `.next` dir.
177-
if (pattern === '*' && base === globBase) {
178-
continue
179-
}
180-
181-
if (pattern === '') {
167+
if (context.compiler.features & Features.Utilities) {
168+
// Add all found files as direct dependencies
169+
for (let file of context.scanner.files) {
182170
result.messages.push({
183171
type: 'dependency',
184172
plugin: '@tailwindcss/postcss',
185-
file: globBase,
186-
parent: result.opts.from,
187-
})
188-
} else {
189-
result.messages.push({
190-
type: 'dir-dependency',
191-
plugin: '@tailwindcss/postcss',
192-
dir: globBase,
193-
glob: pattern,
173+
file,
194174
parent: result.opts.from,
195175
})
196176
}
177+
178+
// Register dependencies so changes in `base` cause a rebuild while
179+
// giving tools like Vite or Parcel a glob that can be used to limit
180+
// the files that cause a rebuild to only those that match it.
181+
for (let { base: globBase, pattern } of context.scanner.globs) {
182+
// Avoid adding a dependency on the base directory itself, since it
183+
// causes Next.js to start an endless recursion if the `distDir` is
184+
// configured to anything other than the default `.next` dir.
185+
if (pattern === '*' && base === globBase) {
186+
continue
187+
}
188+
189+
if (pattern === '') {
190+
result.messages.push({
191+
type: 'dependency',
192+
plugin: '@tailwindcss/postcss',
193+
file: globBase,
194+
parent: result.opts.from,
195+
})
196+
} else {
197+
result.messages.push({
198+
type: 'dir-dependency',
199+
plugin: '@tailwindcss/postcss',
200+
dir: globBase,
201+
glob: pattern,
202+
parent: result.opts.from,
203+
})
204+
}
205+
}
197206
}
198207

199208
env.DEBUG && console.time('[@tailwindcss/postcss] Build CSS')
@@ -237,8 +246,8 @@ function optimizeCss(
237246
nonStandard: {
238247
deepSelectorCombinator: true,
239248
},
240-
include: Features.Nesting,
241-
exclude: Features.LogicalProperties,
249+
include: LightningCssFeatures.Nesting,
250+
exclude: LightningCssFeatures.LogicalProperties,
242251
targets: {
243252
safari: (16 << 16) | (4 << 8),
244253
ios_saf: (16 << 16) | (4 << 8),

packages/@tailwindcss-upgrade/src/template/codemods/theme-to-var.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { toKeyPath } from '../../../../tailwindcss/src/utils/to-key-path'
1313
import * as ValueParser from '../../../../tailwindcss/src/value-parser'
1414
import { printCandidate } from '../candidates'
1515

16-
export enum Convert {
16+
export const enum Convert {
1717
All = 0,
1818
MigrateModifier = 1 << 0,
1919
MigrateThemeOnly = 1 << 1,

packages/@tailwindcss-upgrade/src/utils/walk.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export enum WalkAction {
1+
export const enum WalkAction {
22
// Continue walking the tree. Default behavior.
33
Continue,
44

0 commit comments

Comments
 (0)