Skip to content

Commit

Permalink
Improve performance of @tailwindcss/postcss and @tailwindcss/vite (
Browse files Browse the repository at this point in the history
…#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 <adam.wathan@gmail.com>
  • Loading branch information
RobinMalfait and adamwathan authored Nov 29, 2024
1 parent 6abd808 commit 99b73ee
Show file tree
Hide file tree
Showing 22 changed files with 322 additions and 162 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

- Nothing yet!
### Fixed

- 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))
- Skip reserializing CSS files that don't use Tailwind features in `@tailwindcss/postcss` and `@tailwindcss/vite` ([#15226](https://github.com/tailwindlabs/tailwindcss/pull/15226))

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

Expand Down
3 changes: 3 additions & 0 deletions packages/@tailwindcss-node/src/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import { pathToFileURL } from 'node:url'
import {
__unstable__loadDesignSystem as ___unstable__loadDesignSystem,
compile as _compile,
Features,
} from 'tailwindcss'
import { getModuleDependencies } from './get-module-dependencies'
import { rewriteUrls } from './urls'

export { Features }

export type Resolver = (id: string, base: string) => Promise<string | false | undefined>

export async function compile(
Expand Down
2 changes: 1 addition & 1 deletion packages/@tailwindcss-node/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as Module from 'node:module'
import { pathToFileURL } from 'node:url'
import * as env from './env'
export { __unstable__loadDesignSystem, compile } from './compile'
export { __unstable__loadDesignSystem, compile, Features } from './compile'
export * from './normalize-path'
export { env }

Expand Down
1 change: 1 addition & 0 deletions packages/@tailwindcss-postcss/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"devDependencies": {
"@types/node": "catalog:",
"@types/postcss-import": "14.0.3",
"dedent": "1.5.3",
"internal-example-plugin": "workspace:*",
"postcss-import": "^16.1.0"
}
Expand Down
50 changes: 38 additions & 12 deletions packages/@tailwindcss-postcss/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import dedent from 'dedent'
import { unlink, writeFile } from 'node:fs/promises'
import postcss from 'postcss'
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
Expand All @@ -9,16 +10,20 @@ import tailwindcss from './index'
// We place it in packages/ because Vitest runs in the monorepo root,
// and packages/tailwindcss must be a sub-folder for
// @import 'tailwindcss' to work.
const INPUT_CSS_PATH = `${__dirname}/fixtures/example-project/input.css`
function inputCssFilePath() {
// Including the current test name to ensure that the cache is invalidated per
// test otherwise the cache will be used across tests.
return `${__dirname}/fixtures/example-project/input.css?test=${expect.getState().currentTestName}`
}

const css = String.raw
const css = dedent

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

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

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

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

// `@apply` is used because Lightning is skipped if neither `@tailwind` nor
// `@apply` is used.
let result = await processor.process(
css`
@layer utilities {
Expand All @@ -65,7 +68,7 @@ test('output is optimized by Lightning CSS', async () => {
}
}
`,
{ from: INPUT_CSS_PATH },
{ from: inputCssFilePath() },
)

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

// `@apply` is used because Lightning is skipped if neither `@tailwind` nor
// `@apply` is used.
let result = await processor.process(
css`
@import 'tailwindcss/theme.css' theme(reference);
.foo {
@apply text-red-500;
}
`,
{ from: INPUT_CSS_PATH },
{ from: inputCssFilePath() },
)

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

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

expect(result.css).toContain(
".md\\:\\[\\&\\:hover\\]\\:content-\\[\\'testing_default_base_path\\'\\]",
Expand All @@ -142,7 +143,7 @@ describe('plugins', () => {
@import 'tailwindcss/utilities';
@plugin './plugin.js';
`,
{ from: INPUT_CSS_PATH },
{ from: inputCssFilePath() },
)

expect(result.css.trim()).toMatchInlineSnapshot(`
Expand Down Expand Up @@ -202,7 +203,7 @@ describe('plugins', () => {
@import 'tailwindcss/utilities';
@plugin 'internal-example-plugin';
`,
{ from: INPUT_CSS_PATH },
{ from: inputCssFilePath() },
)

expect(result.css.trim()).toMatchInlineSnapshot(`
Expand All @@ -222,3 +223,28 @@ describe('plugins', () => {
`)
})
})

test('bail early when Tailwind is not used', async () => {
let processor = postcss([
tailwindcss({ base: `${__dirname}/fixtures/example-project`, optimize: { minify: false } }),
])

let result = await processor.process(
css`
.custom-css {
color: red;
}
`,
{ from: inputCssFilePath() },
)

// `fixtures/example-project` includes an `underline` candidate. But since we
// didn't use `@tailwind utilities` we didn't scan for utilities.
expect(result.css).not.toContain('.underline {')

expect(result.css.trim()).toMatchInlineSnapshot(`
".custom-css {
color: red;
}"
`)
})
83 changes: 46 additions & 37 deletions packages/@tailwindcss-postcss/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import QuickLRU from '@alloc/quick-lru'
import { compile, env } from '@tailwindcss/node'
import { compile, env, Features } from '@tailwindcss/node'
import { clearRequireCache } from '@tailwindcss/node/require-cache'
import { Scanner } from '@tailwindcss/oxide'
import { Features, transform } from 'lightningcss'
import { Features as LightningCssFeatures, transform } from 'lightningcss'
import fs from 'node:fs'
import path from 'node:path'
import postcss, { type AcceptedPlugin, type PluginCreator } from 'postcss'
Expand Down Expand Up @@ -63,7 +63,9 @@ function tailwindcss(opts: PluginOptions = {}): AcceptedPlugin {

async function createCompiler() {
env.DEBUG && console.time('[@tailwindcss/postcss] Setup compiler')
clearRequireCache(context.fullRebuildPaths)
if (context.fullRebuildPaths.length > 0 && !isInitialBuild) {
clearRequireCache(context.fullRebuildPaths)
}

context.fullRebuildPaths = []

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

if (context.compiler.features === Features.None) {
return
}

let rebuildStrategy: 'full' | 'incremental' = 'incremental'

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

env.DEBUG && console.time('[@tailwindcss/postcss] Scan for candidates')
let candidates = context.scanner.scan()
let candidates =
context.compiler.features & Features.Utilities ? context.scanner.scan() : []
env.DEBUG && console.timeEnd('[@tailwindcss/postcss] Scan for candidates')

// Add all found files as direct dependencies
for (let file of context.scanner.files) {
result.messages.push({
type: 'dependency',
plugin: '@tailwindcss/postcss',
file,
parent: result.opts.from,
})
}

// Register dependencies so changes in `base` cause a rebuild while
// giving tools like Vite or Parcel a glob that can be used to limit
// the files that cause a rebuild to only those that match it.
for (let { base: globBase, pattern } of context.scanner.globs) {
// Avoid adding a dependency on the base directory itself, since it
// causes Next.js to start an endless recursion if the `distDir` is
// configured to anything other than the default `.next` dir.
if (pattern === '*' && base === globBase) {
continue
}

if (pattern === '') {
if (context.compiler.features & Features.Utilities) {
// Add all found files as direct dependencies
for (let file of context.scanner.files) {
result.messages.push({
type: 'dependency',
plugin: '@tailwindcss/postcss',
file: globBase,
parent: result.opts.from,
})
} else {
result.messages.push({
type: 'dir-dependency',
plugin: '@tailwindcss/postcss',
dir: globBase,
glob: pattern,
file,
parent: result.opts.from,
})
}

// Register dependencies so changes in `base` cause a rebuild while
// giving tools like Vite or Parcel a glob that can be used to limit
// the files that cause a rebuild to only those that match it.
for (let { base: globBase, pattern } of context.scanner.globs) {
// Avoid adding a dependency on the base directory itself, since it
// causes Next.js to start an endless recursion if the `distDir` is
// configured to anything other than the default `.next` dir.
if (pattern === '*' && base === globBase) {
continue
}

if (pattern === '') {
result.messages.push({
type: 'dependency',
plugin: '@tailwindcss/postcss',
file: globBase,
parent: result.opts.from,
})
} else {
result.messages.push({
type: 'dir-dependency',
plugin: '@tailwindcss/postcss',
dir: globBase,
glob: pattern,
parent: result.opts.from,
})
}
}
}

env.DEBUG && console.time('[@tailwindcss/postcss] Build CSS')
Expand Down Expand Up @@ -237,8 +246,8 @@ function optimizeCss(
nonStandard: {
deepSelectorCombinator: true,
},
include: Features.Nesting,
exclude: Features.LogicalProperties,
include: LightningCssFeatures.Nesting,
exclude: LightningCssFeatures.LogicalProperties,
targets: {
safari: (16 << 16) | (4 << 8),
ios_saf: (16 << 16) | (4 << 8),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { toKeyPath } from '../../../../tailwindcss/src/utils/to-key-path'
import * as ValueParser from '../../../../tailwindcss/src/value-parser'
import { printCandidate } from '../candidates'

export enum Convert {
export const enum Convert {
All = 0,
MigrateModifier = 1 << 0,
MigrateThemeOnly = 1 << 1,
Expand Down
2 changes: 1 addition & 1 deletion packages/@tailwindcss-upgrade/src/utils/walk.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export enum WalkAction {
export const enum WalkAction {
// Continue walking the tree. Default behavior.
Continue,

Expand Down
Loading

0 comments on commit 99b73ee

Please sign in to comment.