Skip to content

Commit e9efc22

Browse files
committed
add cloneAstNode helper
Our AST nodes are simple plain objects with a `kind` as the discriminant instead of of an actual class instance. Since we're dealing with simple plain objects, there is no need for a heavy handed `structuredClone` and we can use this function instead. Also added some benchmarks which you can run to see how much the difference is. On my M1 Pro Max while using battery, these are the results: ``` ✓ Cloning AST nodes (2) 2462ms name hz min max mean p75 p99 p995 p999 rme samples · cloneAstNode() 5,769,838.55 0.0000 0.0467 0.0002 0.0002 0.0004 0.0004 0.0005 ±0.09% 2884920 fastest · structuredClone() 222,598.38 0.0042 4.5958 0.0045 0.0045 0.0048 0.0049 0.0100 ±1.81% 111300 BENCH Summary cloneAstNode() - ast.bench.ts > Cloning AST nodes 25.92x faster than structuredClone() ```
1 parent a2d5333 commit e9efc22

File tree

7 files changed

+49
-15
lines changed

7 files changed

+49
-15
lines changed

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: 18 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 } from './ast'
33
import * as CSS from './css-parser'
44

55
const css = String.raw
@@ -19,10 +19,22 @@ 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('structuredClone()', () => {
38+
structuredClone(ast)
39+
})
2840
})

packages/tailwindcss/src/ast.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,26 @@ 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+
case 'at-rule':
129+
case 'at-root':
130+
return { ...node, nodes: node.nodes.map(cloneAstNode) }
131+
132+
case 'context':
133+
return { ...node, context: { ...node.context }, nodes: node.nodes.map(cloneAstNode) }
134+
135+
case 'declaration':
136+
case 'comment':
137+
return { ...node }
138+
139+
default:
140+
node satisfies never
141+
throw new Error(`Unknown node kind: ${(node as any).kind}`)
142+
}
143+
}
144+
125145
export const enum WalkAction {
126146
/** Continue walking, which is the default */
127147
Continue,

packages/tailwindcss/src/compat/container.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { atRule, decl, type AstNode, type AtRule } from '../ast'
1+
import { atRule, cloneAstNode, decl, type AstNode, type AtRule } from '../ast'
22
import type { DesignSystem } from '../design-system'
33
import { compareBreakpoints } from '../utils/compare-breakpoints'
44
import type { ResolvedConfig } from './config/types'
@@ -16,7 +16,7 @@ export function registerContainerCompat(userConfig: ResolvedConfig, designSystem
1616
return
1717
}
1818

19-
designSystem.utilities.static('container', () => structuredClone(rules))
19+
designSystem.utilities.static('container', () => rules.map(cloneAstNode))
2020
}
2121

2222
export function buildCustomContainerUtilityRules(

packages/tailwindcss/src/compat/plugin-api.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Features } from '..'
22
import { substituteAtApply } from '../apply'
3-
import { atRule, decl, rule, walk, type AstNode } from '../ast'
3+
import { atRule, cloneAstNode, decl, rule, walk, type AstNode } from '../ast'
44
import type { Candidate, CandidateModifier, NamedUtilityValue } from '../candidate'
55
import { substituteFunctions } from '../css-functions'
66
import * as CSS from '../css-parser'
@@ -329,7 +329,7 @@ export function buildPluginApi({
329329
}
330330

331331
designSystem.utilities.static(className, (candidate) => {
332-
let clonedAst = structuredClone(ast)
332+
let clonedAst = ast.map(cloneAstNode)
333333
replaceNestedClassNameReferences(clonedAst, className, candidate.raw)
334334
featuresRef.current |= substituteAtApply(clonedAst, designSystem)
335335
return clonedAst

packages/tailwindcss/src/utilities.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
atRoot,
33
atRule,
4+
cloneAstNode,
45
decl,
56
rule,
67
styleRule,
@@ -6008,7 +6009,7 @@ export function createCssUtility(node: AtRule) {
60086009
})
60096010

60106011
designSystem.utilities.functional(name.slice(0, -2), (candidate) => {
6011-
let atRule = structuredClone(node)
6012+
let atRule = cloneAstNode(node)
60126013

60136014
let value = candidate.value
60146015
let modifier = candidate.modifier
@@ -6176,7 +6177,7 @@ export function createCssUtility(node: AtRule) {
61766177

61776178
if (IS_VALID_STATIC_UTILITY_NAME.test(name)) {
61786179
return (designSystem: DesignSystem) => {
6179-
designSystem.utilities.static(name, () => structuredClone(node.nodes))
6180+
designSystem.utilities.static(name, () => node.nodes.map(cloneAstNode))
61806181
}
61816182
}
61826183

packages/tailwindcss/src/variants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
WalkAction,
44
atRoot,
55
atRule,
6+
cloneAstNode,
67
decl,
78
rule,
89
styleRule,
@@ -100,7 +101,7 @@ export class Variants {
100101
this.static(
101102
name,
102103
(r) => {
103-
let body = structuredClone(ast)
104+
let body = ast.map(cloneAstNode)
104105
if (usesAtVariant) substituteAtVariant(body, designSystem)
105106
substituteAtSlot(body, r.nodes)
106107
r.nodes = body

0 commit comments

Comments
 (0)