From 28f24af15d3f4b2147a6bdb28221c114e6c3effc Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 2 Jan 2025 12:28:49 +0100 Subject: [PATCH 1/4] remove `parentPath` allocations Instead of creating a new path over and over again, we can instead push the node before the next loop starts, then pop it when we're done. --- packages/tailwindcss/src/ast.ts | 28 +++++++++++-------- .../src/compat/config/deep-merge.ts | 14 ++++------ 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 74a2bc63eb7c..8f734e46ae9f 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -119,26 +119,24 @@ export function walk( path: AstNode[] }, ) => void | WalkAction, - parentPath: AstNode[] = [], + path: AstNode[] = [], context: Record = {}, ) { 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, @@ -152,6 +150,7 @@ export function walk( i-- }, }) ?? WalkAction.Continue + path.pop() // Stop the walk entirely if (status === WalkAction.Stop) return WalkAction.Stop @@ -160,7 +159,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,21 +182,23 @@ export function walkDepth( replaceWith(newNode: AstNode[]): void }, ) => void, - parentPath: AstNode[] = [], + path: AstNode[] = [], context: Record = {}, ) { 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, @@ -205,6 +210,7 @@ export function walkDepth( i += newNode.length - 1 }, }) + path.pop() } } diff --git a/packages/tailwindcss/src/compat/config/deep-merge.ts b/packages/tailwindcss/src/compat/config/deep-merge.ts index 3bbae51ddcef..2bf15c9b3b00 100644 --- a/packages/tailwindcss/src/compat/config/deep-merge.ts +++ b/packages/tailwindcss/src/compat/config/deep-merge.ts @@ -11,7 +11,7 @@ export function deepMerge( target: T, sources: (Partial | 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( } 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() } } From f32acde921cfb545bb4d1e6c148d7e014a34ffe3 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Thu, 2 Jan 2025 12:50:26 +0100 Subject: [PATCH 2/4] use direct replacements if possible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of always calling `.splice()`, there are cases where if we replace a node with a single other note that we can replace it directly without using `splice(…)`. --- packages/tailwindcss/src/ast.ts | 25 +++++++++++++++++-- .../tailwindcss/src/compat/selector-parser.ts | 13 +++++++++- packages/tailwindcss/src/value-parser.ts | 13 +++++++++- 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/packages/tailwindcss/src/ast.ts b/packages/tailwindcss/src/ast.ts index 8f734e46ae9f..2fe9d6901a4f 100644 --- a/packages/tailwindcss/src/ast.ts +++ b/packages/tailwindcss/src/ast.ts @@ -143,7 +143,18 @@ export function walk( 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. @@ -204,7 +215,17 @@ export function walkDepth( 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 diff --git a/packages/tailwindcss/src/compat/selector-parser.ts b/packages/tailwindcss/src/compat/selector-parser.ts index 7dc0b82f8f1a..f62e0aed4cec 100644 --- a/packages/tailwindcss/src/compat/selector-parser.ts +++ b/packages/tailwindcss/src/compat/selector-parser.ts @@ -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. diff --git a/packages/tailwindcss/src/value-parser.ts b/packages/tailwindcss/src/value-parser.ts index d51b793cb254..80a7827b2e8a 100644 --- a/packages/tailwindcss/src/value-parser.ts +++ b/packages/tailwindcss/src/value-parser.ts @@ -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. From edfee392e158325fdbc923a5685e1eaa19934e65 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 6 Jan 2025 10:53:16 +0100 Subject: [PATCH 3/4] update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index df9331a4b8ad..08484cdc7474 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Don’t detect arbitrary properties when preceded by an escape ([#15456](https://github.com/tailwindlabs/tailwindcss/pull/15456)) - 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)) +- Improve `walk(…)` performance and memory usage ([#15529](https://github.com/tailwindlabs/tailwindcss/pull/15529)) ### Changed @@ -780,3 +781,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 + From d28897b7d585927011092feda0046377fa36e3a3 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Mon, 6 Jan 2025 15:25:23 +0100 Subject: [PATCH 4/4] Update CHANGELOG.md Co-authored-by: Philipp Spiess --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2892d0752b27..261e67188b24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +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 `walk(…)` performance and memory usage ([#15529](https://github.com/tailwindlabs/tailwindcss/pull/15529)) +- Improve performance and memory usage ([#15529](https://github.com/tailwindlabs/tailwindcss/pull/15529)) ### Changed