Skip to content

Commit efe084b

Browse files
authored
Improve performance of cloning AST nodes (#19067)
This PR improves the performance of when we need to clone some AST nodes. We have a few places where we clone `Candidate`, `Variant` and CSS `AST` nodes. Right now we use `structuredClone`, which works, but it is a generic solution. However, we do know the exact structure of these AST nodes, so we can write specialized clone functions that are much faster. ## Test plan 1. All the tests still pass with this change 2. The performance is better: ``` cloneCandidate - src/candidate.bench.ts > Candidate cloning 1.72x faster than cloneCandidate (spread) 74.03x faster than structuredClone cloneAstNode() - src/ast.bench.ts > Cloning AST nodes 1.15x faster than cloneAstNode (with spread) 33.54x faster than structuredClone() ``` Ready for review, but should be merged after #19059
1 parent b77971f commit efe084b

18 files changed

+312
-37
lines changed

packages/@tailwindcss-upgrade/src/codemods/template/candidates.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Scanner } from '@tailwindcss/oxide'
2-
import type { Candidate } from '../../../../tailwindcss/src/candidate'
2+
import { cloneCandidate, type Candidate } from '../../../../tailwindcss/src/candidate'
33
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
44

55
export async function extractRawCandidates(
@@ -18,7 +18,7 @@ export async function extractRawCandidates(
1818

1919
// Create a basic stripped candidate without variants or important flag
2020
export function baseCandidate<T extends Candidate>(candidate: T) {
21-
let base = structuredClone(candidate)
21+
let base = cloneCandidate(candidate)
2222

2323
base.important = false
2424
base.variants = []

packages/@tailwindcss-upgrade/src/codemods/template/migrate-arbitrary-variants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { cloneCandidate } from '../../../../tailwindcss/src/candidate'
12
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
23
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
34
import {
@@ -22,7 +23,7 @@ export function migrateArbitraryVariants(
2223

2324
// The below logic makes use of mutation. Since candidates in the
2425
// DesignSystem are cached, we can't mutate them directly.
25-
let candidate = structuredClone(readonlyCandidate) as Writable<typeof readonlyCandidate>
26+
let candidate = cloneCandidate(readonlyCandidate) as Writable<typeof readonlyCandidate>
2627

2728
for (let [variant] of walkVariants(candidate)) {
2829
if (variant.kind === 'compound') continue

packages/@tailwindcss-upgrade/src/codemods/template/migrate-automatic-var-injection.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { walk, WalkAction } from '../../../../tailwindcss/src/ast'
2-
import { type Candidate, type Variant } from '../../../../tailwindcss/src/candidate'
2+
import { cloneCandidate, type Candidate, type Variant } from '../../../../tailwindcss/src/candidate'
33
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
44
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
5+
import type { Writable } from '../../../../tailwindcss/src/types'
56
import * as ValueParser from '../../../../tailwindcss/src/value-parser'
67

78
export function migrateAutomaticVarInjection(
@@ -12,7 +13,7 @@ export function migrateAutomaticVarInjection(
1213
for (let readonlyCandidate of designSystem.parseCandidate(rawCandidate)) {
1314
// The below logic makes extended use of mutation. Since candidates in the
1415
// DesignSystem are cached, we can't mutate them directly.
15-
let candidate = structuredClone(readonlyCandidate) as Candidate
16+
let candidate = cloneCandidate(readonlyCandidate) as Writable<Candidate>
1617

1718
let didChange = false
1819

packages/@tailwindcss-upgrade/src/codemods/template/migrate-camelcase-in-named-value.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { cloneCandidate } from '../../../../tailwindcss/src/candidate'
12
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
23
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
34
import * as version from '../../utils/version'
@@ -15,7 +16,7 @@ export function migrateCamelcaseInNamedValue(
1516

1617
for (let candidate of designSystem.parseCandidate(rawCandidate)) {
1718
if (candidate.kind !== 'functional') continue
18-
let clone = structuredClone(candidate)
19+
let clone = cloneCandidate(candidate)
1920
let didChange = false
2021

2122
if (

packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-arbitrary-values.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { parseCandidate } from '../../../../tailwindcss/src/candidate'
1+
import { cloneCandidate, parseCandidate } from '../../../../tailwindcss/src/candidate'
22
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
33
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
44
import { segment } from '../../../../tailwindcss/src/utils/segment'
@@ -9,7 +9,7 @@ export function migrateLegacyArbitraryValues(
99
rawCandidate: string,
1010
): string {
1111
for (let candidate of parseCandidate(rawCandidate, designSystem)) {
12-
let clone = structuredClone(candidate)
12+
let clone = cloneCandidate(candidate)
1313
let changed = false
1414

1515
// Convert commas to spaces. E.g.: [auto,1fr] to [auto_1fr]

packages/@tailwindcss-upgrade/src/codemods/template/migrate-legacy-classes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { __unstable__loadDesignSystem } from '@tailwindcss/node'
22
import path from 'node:path'
33
import url from 'node:url'
4-
import type { Candidate } from '../../../../tailwindcss/src/candidate'
4+
import { cloneCandidate, type Candidate } from '../../../../tailwindcss/src/candidate'
55
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
66
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
77
import { DefaultMap } from '../../../../tailwindcss/src/utils/default-map'
@@ -100,7 +100,7 @@ export async function migrateLegacyClasses(
100100

101101
// Re-apply the variants and important flag from the original candidate.
102102
// E.g.: `hover:blur!` -> `blur` -> `blur-sm` -> `hover:blur-sm!`
103-
let newCandidate = structuredClone(newBaseCandidate) as Candidate
103+
let newCandidate = cloneCandidate(newBaseCandidate) as Candidate
104104
newCandidate.variants = candidate.variants
105105
newCandidate.important = candidate.important
106106

packages/@tailwindcss-upgrade/src/codemods/template/migrate-modernize-arbitrary-values.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { parseCandidate } from '../../../../tailwindcss/src/candidate'
1+
import { cloneCandidate, parseCandidate } from '../../../../tailwindcss/src/candidate'
22
import type { Config } from '../../../../tailwindcss/src/compat/plugin-api'
33
import type { DesignSystem } from '../../../../tailwindcss/src/design-system'
44
import { replaceObject } from '../../../../tailwindcss/src/utils/replace-object'
@@ -10,7 +10,7 @@ export function migrateModernizeArbitraryValues(
1010
rawCandidate: string,
1111
): string {
1212
for (let candidate of parseCandidate(rawCandidate, designSystem)) {
13-
let clone = structuredClone(candidate)
13+
let clone = cloneCandidate(candidate)
1414
let changed = false
1515

1616
for (let [variant] of walkVariants(clone)) {

packages/tailwindcss/src/apply.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Features } from '.'
2-
import { rule, toCss, walk, WalkAction, type AstNode } from './ast'
2+
import { cloneAstNode, rule, toCss, walk, WalkAction, type AstNode } from './ast'
33
import { compileCandidates } from './compile'
44
import type { DesignSystem } from './design-system'
55
import type { SourceLocation } from './source-maps/source'
@@ -249,7 +249,7 @@ export function substituteAtApply(ast: AstNode[], designSystem: DesignSystem) {
249249
let candidate = compiled.nodeSorting.get(node)?.candidate
250250
let candidateOffset = candidate ? candidateOffsets[candidate] : undefined
251251

252-
node = structuredClone(node)
252+
node = cloneAstNode(node)
253253

254254
if (!src || !candidate || candidateOffset === undefined) {
255255
// While the original nodes may have come from an `@utility` we still

packages/tailwindcss/src/ast.bench.ts

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { bench } from 'vitest'
2-
import { toCss } from './ast'
1+
import { bench, describe } from 'vitest'
2+
import { cloneAstNode, toCss, type AstNode } from './ast'
33
import * as CSS from './css-parser'
44

55
const css = String.raw
@@ -19,10 +19,46 @@ const input = css`
1919
`
2020
const ast = CSS.parse(input)
2121

22-
bench('toCss', () => {
23-
toCss(ast)
22+
describe('AST to CSS', () => {
23+
bench('toCss', () => {
24+
toCss(ast)
25+
})
26+
27+
bench('toCss with source maps', () => {
28+
toCss(ast, true)
29+
})
2430
})
2531

26-
bench('toCss with source maps', () => {
27-
toCss(ast, true)
32+
describe('Cloning AST nodes', () => {
33+
bench('cloneAstNode()', () => {
34+
ast.map(cloneAstNode)
35+
})
36+
37+
bench('cloneAstNode (with spread)', () => {
38+
ast.map(cloneAstNodeSpread)
39+
})
40+
41+
bench('structuredClone()', () => {
42+
structuredClone(ast)
43+
})
2844
})
45+
46+
function cloneAstNodeSpread<T extends AstNode>(node: T): T {
47+
switch (node.kind) {
48+
case 'rule':
49+
case 'at-rule':
50+
case 'at-root':
51+
return { ...node, nodes: node.nodes.map(cloneAstNodeSpread) }
52+
53+
case 'context':
54+
return { ...node, context: { ...node.context }, nodes: node.nodes.map(cloneAstNodeSpread) }
55+
56+
case 'declaration':
57+
case 'comment':
58+
return { ...node }
59+
60+
default:
61+
node satisfies never
62+
throw new Error(`Unknown node kind: ${(node as any).kind}`)
63+
}
64+
}

packages/tailwindcss/src/ast.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,68 @@ export function atRoot(nodes: AstNode[]): AtRoot {
122122
}
123123
}
124124

125+
export function cloneAstNode<T extends AstNode>(node: T): T {
126+
switch (node.kind) {
127+
case 'rule':
128+
return {
129+
kind: node.kind,
130+
selector: node.selector,
131+
nodes: node.nodes.map(cloneAstNode),
132+
src: node.src,
133+
dst: node.dst,
134+
} satisfies StyleRule as T
135+
136+
case 'at-rule':
137+
return {
138+
kind: node.kind,
139+
name: node.name,
140+
params: node.params,
141+
nodes: node.nodes.map(cloneAstNode),
142+
src: node.src,
143+
dst: node.dst,
144+
} satisfies AtRule as T
145+
146+
case 'at-root':
147+
return {
148+
kind: node.kind,
149+
nodes: node.nodes.map(cloneAstNode),
150+
src: node.src,
151+
dst: node.dst,
152+
} satisfies AtRoot as T
153+
154+
case 'context':
155+
return {
156+
kind: node.kind,
157+
context: { ...node.context },
158+
nodes: node.nodes.map(cloneAstNode),
159+
src: node.src,
160+
dst: node.dst,
161+
} satisfies Context as T
162+
163+
case 'declaration':
164+
return {
165+
kind: node.kind,
166+
property: node.property,
167+
value: node.value,
168+
important: node.important,
169+
src: node.src,
170+
dst: node.dst,
171+
} satisfies Declaration as T
172+
173+
case 'comment':
174+
return {
175+
kind: node.kind,
176+
value: node.value,
177+
src: node.src,
178+
dst: node.dst,
179+
} satisfies Comment as T
180+
181+
default:
182+
node satisfies never
183+
throw new Error(`Unknown node kind: ${(node as any).kind}`)
184+
}
185+
}
186+
125187
export const enum WalkAction {
126188
/** Continue walking, which is the default */
127189
Continue,

0 commit comments

Comments
 (0)