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:  --------- Co-authored-by: Philipp Spiess <hello@philippspiess.com>
This commit is contained in:
parent
8299d0486b
commit
af3345189a
5 changed files with 71 additions and 24 deletions
|
|
@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- 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))
|
||||
- Fix `inset-shadow-*` suggestions in IntelliSense ([#15471](https://github.com/tailwindlabs/tailwindcss/pull/15471))
|
||||
- Only compile arbitrary values ending in `]` ([#15503](https://github.com/tailwindlabs/tailwindcss/pull/15503))
|
||||
- Improve performance and memory usage ([#15529](https://github.com/tailwindlabs/tailwindcss/pull/15529))
|
||||
|
||||
### Changed
|
||||
|
||||
|
|
@ -781,3 +782,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
## [4.0.0-alpha.1] - 2024-03-06
|
||||
|
||||
- First 4.0.0-alpha.1 release
|
||||
|
||||
|
|
|
|||
|
|
@ -119,39 +119,49 @@ export function walk(
|
|||
path: AstNode[]
|
||||
},
|
||||
) => void | WalkAction,
|
||||
parentPath: AstNode[] = [],
|
||||
path: AstNode[] = [],
|
||||
context: Record<string, string | boolean> = {},
|
||||
) {
|
||||
for (let i = 0; i < ast.length; i++) {
|
||||
let node = ast[i]
|
||||
let path = [...parentPath, node]
|
||||
let parent = parentPath.at(-1) ?? null
|
||||
let parent = path[path.length - 1] ?? null
|
||||
|
||||
// We want context nodes to be transparent in walks. This means that
|
||||
// whenever we encounter one, we immediately walk through its children and
|
||||
// furthermore we also don't update the parent.
|
||||
if (node.kind === 'context') {
|
||||
if (
|
||||
walk(node.nodes, visit, parentPath, { ...context, ...node.context }) === WalkAction.Stop
|
||||
) {
|
||||
if (walk(node.nodes, visit, path, { ...context, ...node.context }) === WalkAction.Stop) {
|
||||
return WalkAction.Stop
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
path.push(node)
|
||||
let status =
|
||||
visit(node, {
|
||||
parent,
|
||||
context,
|
||||
path,
|
||||
replaceWith(newNode) {
|
||||
ast.splice(i, 1, ...(Array.isArray(newNode) ? newNode : [newNode]))
|
||||
if (Array.isArray(newNode)) {
|
||||
if (newNode.length === 0) {
|
||||
ast.splice(i, 1)
|
||||
} else if (newNode.length === 1) {
|
||||
ast[i] = newNode[0]
|
||||
} else {
|
||||
ast.splice(i, 1, ...newNode)
|
||||
}
|
||||
} else {
|
||||
ast[i] = newNode
|
||||
}
|
||||
|
||||
// We want to visit the newly replaced node(s), which start at the
|
||||
// current index (i). By decrementing the index here, the next loop
|
||||
// will process this position (containing the replaced node) again.
|
||||
i--
|
||||
},
|
||||
}) ?? WalkAction.Continue
|
||||
path.pop()
|
||||
|
||||
// Stop the walk entirely
|
||||
if (status === WalkAction.Stop) return WalkAction.Stop
|
||||
|
|
@ -160,7 +170,11 @@ export function walk(
|
|||
if (status === WalkAction.Skip) continue
|
||||
|
||||
if (node.kind === 'rule' || node.kind === 'at-rule') {
|
||||
if (walk(node.nodes, visit, path, context) === WalkAction.Stop) {
|
||||
path.push(node)
|
||||
let result = walk(node.nodes, visit, path, context)
|
||||
path.pop()
|
||||
|
||||
if (result === WalkAction.Stop) {
|
||||
return WalkAction.Stop
|
||||
}
|
||||
}
|
||||
|
|
@ -179,32 +193,45 @@ export function walkDepth(
|
|||
replaceWith(newNode: AstNode[]): void
|
||||
},
|
||||
) => void,
|
||||
parentPath: AstNode[] = [],
|
||||
path: AstNode[] = [],
|
||||
context: Record<string, string | boolean> = {},
|
||||
) {
|
||||
for (let i = 0; i < ast.length; i++) {
|
||||
let node = ast[i]
|
||||
let path = [...parentPath, node]
|
||||
let parent = parentPath.at(-1) ?? null
|
||||
let parent = path[path.length - 1] ?? null
|
||||
|
||||
if (node.kind === 'rule' || node.kind === 'at-rule') {
|
||||
path.push(node)
|
||||
walkDepth(node.nodes, visit, path, context)
|
||||
path.pop()
|
||||
} else if (node.kind === 'context') {
|
||||
walkDepth(node.nodes, visit, parentPath, { ...context, ...node.context })
|
||||
walkDepth(node.nodes, visit, path, { ...context, ...node.context })
|
||||
continue
|
||||
}
|
||||
|
||||
path.push(node)
|
||||
visit(node, {
|
||||
parent,
|
||||
context,
|
||||
path,
|
||||
replaceWith(newNode) {
|
||||
ast.splice(i, 1, ...newNode)
|
||||
if (Array.isArray(newNode)) {
|
||||
if (newNode.length === 0) {
|
||||
ast.splice(i, 1)
|
||||
} else if (newNode.length === 1) {
|
||||
ast[i] = newNode[0]
|
||||
} else {
|
||||
ast.splice(i, 1, ...newNode)
|
||||
}
|
||||
} else {
|
||||
ast[i] = newNode
|
||||
}
|
||||
|
||||
// Skip over the newly inserted nodes (being depth-first it doesn't make sense to visit them)
|
||||
i += newNode.length - 1
|
||||
},
|
||||
})
|
||||
path.pop()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export function deepMerge<T extends object>(
|
|||
target: T,
|
||||
sources: (Partial<T> | null | undefined)[],
|
||||
customizer: (a: any, b: any, keypath: (keyof T)[]) => any,
|
||||
parentPath: (keyof T)[] = [],
|
||||
path: (keyof T)[] = [],
|
||||
) {
|
||||
type Key = keyof T
|
||||
type Value = T[Key]
|
||||
|
|
@ -22,21 +22,17 @@ export function deepMerge<T extends object>(
|
|||
}
|
||||
|
||||
for (let k of Reflect.ownKeys(source) as Key[]) {
|
||||
let currentParentPath = [...parentPath, k]
|
||||
let merged = customizer(target[k], source[k], currentParentPath)
|
||||
path.push(k)
|
||||
let merged = customizer(target[k], source[k], path)
|
||||
|
||||
if (merged !== undefined) {
|
||||
target[k] = merged
|
||||
} else if (!isPlainObject(target[k]) || !isPlainObject(source[k])) {
|
||||
target[k] = source[k] as Value
|
||||
} else {
|
||||
target[k] = deepMerge(
|
||||
{},
|
||||
[target[k], source[k]],
|
||||
customizer,
|
||||
currentParentPath as any,
|
||||
) as Value
|
||||
target[k] = deepMerge({}, [target[k], source[k]], customizer, path as any) as Value
|
||||
}
|
||||
path.pop()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,18 @@ export function walk(
|
|||
visit(node, {
|
||||
parent,
|
||||
replaceWith(newNode) {
|
||||
ast.splice(i, 1, ...(Array.isArray(newNode) ? newNode : [newNode]))
|
||||
if (Array.isArray(newNode)) {
|
||||
if (newNode.length === 0) {
|
||||
ast.splice(i, 1)
|
||||
} else if (newNode.length === 1) {
|
||||
ast[i] = newNode[0]
|
||||
} else {
|
||||
ast.splice(i, 1, ...newNode)
|
||||
}
|
||||
} else {
|
||||
ast[i] = newNode
|
||||
}
|
||||
|
||||
// We want to visit the newly replaced node(s), which start at the
|
||||
// current index (i). By decrementing the index here, the next loop
|
||||
// will process this position (containing the replaced node) again.
|
||||
|
|
|
|||
|
|
@ -67,7 +67,18 @@ export function walk(
|
|||
visit(node, {
|
||||
parent,
|
||||
replaceWith(newNode) {
|
||||
ast.splice(i, 1, ...(Array.isArray(newNode) ? newNode : [newNode]))
|
||||
if (Array.isArray(newNode)) {
|
||||
if (newNode.length === 0) {
|
||||
ast.splice(i, 1)
|
||||
} else if (newNode.length === 1) {
|
||||
ast[i] = newNode[0]
|
||||
} else {
|
||||
ast.splice(i, 1, ...newNode)
|
||||
}
|
||||
} else {
|
||||
ast[i] = newNode
|
||||
}
|
||||
|
||||
// We want to visit the newly replaced node(s), which start at the
|
||||
// current index (i). By decrementing the index here, the next loop
|
||||
// will process this position (containing the replaced node) again.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue