Skip to content

Commit af33451

Browse files
Improve walk(…) performance (#15529)
This PR is a tiny improvement to the `walk(…)` implementations, not a super big deal but thought about something and was pleasently surprised that it did have an impact. The idea is twofold: 1. Reduce array allocations while walking to build a `path` to the current node. This re-uses the existing `path` array and pushes the current node before the recursive call and pops it afterwards. This way we don't need to allocate a new array for each recursive call. Testing this on Tailwind UI means ~14k fewer allocations. 2. Instead of always calling `.splice(…)`, we can directly update a single value in the array if we are replacing a node with another node. Testing on the Tailwind UI codebase, this results in: ![image](https://github.com/user-attachments/assets/5a1c2102-1493-410f-b527-847fb4a75b31) --------- Co-authored-by: Philipp Spiess <[email protected]>
1 parent 8299d04 commit af33451

File tree

5 files changed

+71
-24
lines changed

5 files changed

+71
-24
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Fix incorrectly named `bg-round` and `bg-space` utilities to `bg-repeat-round` to `bg-repeat-space` ([#15462](https://github.com/tailwindlabs/tailwindcss/pull/15462))
1515
- Fix `inset-shadow-*` suggestions in IntelliSense ([#15471](https://github.com/tailwindlabs/tailwindcss/pull/15471))
1616
- Only compile arbitrary values ending in `]` ([#15503](https://github.com/tailwindlabs/tailwindcss/pull/15503))
17+
- Improve performance and memory usage ([#15529](https://github.com/tailwindlabs/tailwindcss/pull/15529))
1718

1819
### Changed
1920

@@ -781,3 +782,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
781782
## [4.0.0-alpha.1] - 2024-03-06
782783

783784
- First 4.0.0-alpha.1 release
785+

packages/tailwindcss/src/ast.ts

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -119,39 +119,49 @@ export function walk(
119119
path: AstNode[]
120120
},
121121
) => void | WalkAction,
122-
parentPath: AstNode[] = [],
122+
path: AstNode[] = [],
123123
context: Record<string, string | boolean> = {},
124124
) {
125125
for (let i = 0; i < ast.length; i++) {
126126
let node = ast[i]
127-
let path = [...parentPath, node]
128-
let parent = parentPath.at(-1) ?? null
127+
let parent = path[path.length - 1] ?? null
129128

130129
// We want context nodes to be transparent in walks. This means that
131130
// whenever we encounter one, we immediately walk through its children and
132131
// furthermore we also don't update the parent.
133132
if (node.kind === 'context') {
134-
if (
135-
walk(node.nodes, visit, parentPath, { ...context, ...node.context }) === WalkAction.Stop
136-
) {
133+
if (walk(node.nodes, visit, path, { ...context, ...node.context }) === WalkAction.Stop) {
137134
return WalkAction.Stop
138135
}
139136
continue
140137
}
141138

139+
path.push(node)
142140
let status =
143141
visit(node, {
144142
parent,
145143
context,
146144
path,
147145
replaceWith(newNode) {
148-
ast.splice(i, 1, ...(Array.isArray(newNode) ? newNode : [newNode]))
146+
if (Array.isArray(newNode)) {
147+
if (newNode.length === 0) {
148+
ast.splice(i, 1)
149+
} else if (newNode.length === 1) {
150+
ast[i] = newNode[0]
151+
} else {
152+
ast.splice(i, 1, ...newNode)
153+
}
154+
} else {
155+
ast[i] = newNode
156+
}
157+
149158
// We want to visit the newly replaced node(s), which start at the
150159
// current index (i). By decrementing the index here, the next loop
151160
// will process this position (containing the replaced node) again.
152161
i--
153162
},
154163
}) ?? WalkAction.Continue
164+
path.pop()
155165

156166
// Stop the walk entirely
157167
if (status === WalkAction.Stop) return WalkAction.Stop
@@ -160,7 +170,11 @@ export function walk(
160170
if (status === WalkAction.Skip) continue
161171

162172
if (node.kind === 'rule' || node.kind === 'at-rule') {
163-
if (walk(node.nodes, visit, path, context) === WalkAction.Stop) {
173+
path.push(node)
174+
let result = walk(node.nodes, visit, path, context)
175+
path.pop()
176+
177+
if (result === WalkAction.Stop) {
164178
return WalkAction.Stop
165179
}
166180
}
@@ -179,32 +193,45 @@ export function walkDepth(
179193
replaceWith(newNode: AstNode[]): void
180194
},
181195
) => void,
182-
parentPath: AstNode[] = [],
196+
path: AstNode[] = [],
183197
context: Record<string, string | boolean> = {},
184198
) {
185199
for (let i = 0; i < ast.length; i++) {
186200
let node = ast[i]
187-
let path = [...parentPath, node]
188-
let parent = parentPath.at(-1) ?? null
201+
let parent = path[path.length - 1] ?? null
189202

190203
if (node.kind === 'rule' || node.kind === 'at-rule') {
204+
path.push(node)
191205
walkDepth(node.nodes, visit, path, context)
206+
path.pop()
192207
} else if (node.kind === 'context') {
193-
walkDepth(node.nodes, visit, parentPath, { ...context, ...node.context })
208+
walkDepth(node.nodes, visit, path, { ...context, ...node.context })
194209
continue
195210
}
196211

212+
path.push(node)
197213
visit(node, {
198214
parent,
199215
context,
200216
path,
201217
replaceWith(newNode) {
202-
ast.splice(i, 1, ...newNode)
218+
if (Array.isArray(newNode)) {
219+
if (newNode.length === 0) {
220+
ast.splice(i, 1)
221+
} else if (newNode.length === 1) {
222+
ast[i] = newNode[0]
223+
} else {
224+
ast.splice(i, 1, ...newNode)
225+
}
226+
} else {
227+
ast[i] = newNode
228+
}
203229

204230
// Skip over the newly inserted nodes (being depth-first it doesn't make sense to visit them)
205231
i += newNode.length - 1
206232
},
207233
})
234+
path.pop()
208235
}
209236
}
210237

packages/tailwindcss/src/compat/config/deep-merge.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export function deepMerge<T extends object>(
1111
target: T,
1212
sources: (Partial<T> | null | undefined)[],
1313
customizer: (a: any, b: any, keypath: (keyof T)[]) => any,
14-
parentPath: (keyof T)[] = [],
14+
path: (keyof T)[] = [],
1515
) {
1616
type Key = keyof T
1717
type Value = T[Key]
@@ -22,21 +22,17 @@ export function deepMerge<T extends object>(
2222
}
2323

2424
for (let k of Reflect.ownKeys(source) as Key[]) {
25-
let currentParentPath = [...parentPath, k]
26-
let merged = customizer(target[k], source[k], currentParentPath)
25+
path.push(k)
26+
let merged = customizer(target[k], source[k], path)
2727

2828
if (merged !== undefined) {
2929
target[k] = merged
3030
} else if (!isPlainObject(target[k]) || !isPlainObject(source[k])) {
3131
target[k] = source[k] as Value
3232
} else {
33-
target[k] = deepMerge(
34-
{},
35-
[target[k], source[k]],
36-
customizer,
37-
currentParentPath as any,
38-
) as Value
33+
target[k] = deepMerge({}, [target[k], source[k]], customizer, path as any) as Value
3934
}
35+
path.pop()
4036
}
4137
}
4238

packages/tailwindcss/src/compat/selector-parser.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,18 @@ export function walk(
9696
visit(node, {
9797
parent,
9898
replaceWith(newNode) {
99-
ast.splice(i, 1, ...(Array.isArray(newNode) ? newNode : [newNode]))
99+
if (Array.isArray(newNode)) {
100+
if (newNode.length === 0) {
101+
ast.splice(i, 1)
102+
} else if (newNode.length === 1) {
103+
ast[i] = newNode[0]
104+
} else {
105+
ast.splice(i, 1, ...newNode)
106+
}
107+
} else {
108+
ast[i] = newNode
109+
}
110+
100111
// We want to visit the newly replaced node(s), which start at the
101112
// current index (i). By decrementing the index here, the next loop
102113
// will process this position (containing the replaced node) again.

packages/tailwindcss/src/value-parser.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,18 @@ export function walk(
6767
visit(node, {
6868
parent,
6969
replaceWith(newNode) {
70-
ast.splice(i, 1, ...(Array.isArray(newNode) ? newNode : [newNode]))
70+
if (Array.isArray(newNode)) {
71+
if (newNode.length === 0) {
72+
ast.splice(i, 1)
73+
} else if (newNode.length === 1) {
74+
ast[i] = newNode[0]
75+
} else {
76+
ast.splice(i, 1, ...newNode)
77+
}
78+
} else {
79+
ast[i] = newNode
80+
}
81+
7182
// We want to visit the newly replaced node(s), which start at the
7283
// current index (i). By decrementing the index here, the next loop
7384
// will process this position (containing the replaced node) again.

0 commit comments

Comments
 (0)