From 1ceea2ff774ebafeaff13d643714ac8e23c93f89 Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 15 Oct 2025 18:50:09 +0200 Subject: [PATCH 1/2] change underlying data structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This improves memory usage and performance. The idea is exactly the same though. But instead of using `push()` and `pop()` we just build up 2 objects where we can track the number and parent individually based on the `depth`. This means that we only allocate 2 objects (which we change over time) we then simply use `parents[depth]` and `offsets[depth]` to get the necessary data out. Additionally, we're only storing the index/offset and the parent now. Since the incoming ast is `T[]`, it means that we don't really have a parent. For this we can create a fake parent (which is never passed to `enter(…)`, `exit(…)` or used by `path()`) --- packages/tailwindcss/src/walk.ts | 50 +++++++++++++++++++------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/packages/tailwindcss/src/walk.ts b/packages/tailwindcss/src/walk.ts index ccb384256c3f..daef318d7472 100644 --- a/packages/tailwindcss/src/walk.ts +++ b/packages/tailwindcss/src/walk.ts @@ -57,36 +57,44 @@ function walkImplementation( enter: (node: T, ctx: VisitContext) => EnterResult | void = () => WalkAction.Continue, exit: (node: T, ctx: VisitContext) => ExitResult | void = () => WalkAction.Continue, ) { - let stack: [nodes: T[], offset: number, parent: Parent | null][] = [[ast, 0, null]] + let surrogate = { nodes: ast } as Parent + + // Reduce memory usage by tracking 2 different objects instead of a single + // stack data structure. We could use 2 arrays, but objects are faster in Bun. + // In Node.js the 2 arrays or 2 objects have similar performance. + // + // Used indexing to prevent `push()` / `pop()` overhead. + let offsets: Record = { 0: 0 } + let parents: Record> = { 0: surrogate } + + let depth = 0 + let ctx: VisitContext = { parent: null, depth: 0, path() { let path: T[] = [] - for (let i = 1; i < stack.length; i++) { - let parent = stack[i][2] - if (parent) path.push(parent) + for (let i = 1; i <= depth; i++) { + path.push(parents[i]) } return path }, } - while (stack.length > 0) { - let depth = stack.length - 1 - let frame = stack[depth] - let nodes = frame[0] - let offset = frame[1] - let parent = frame[2] + while (depth >= 0) { + let offset = offsets[depth] + let parent = parents[depth] + let nodes = parent.nodes // Done with this level if (offset >= nodes.length) { - stack.pop() + depth-- continue } - ctx.parent = parent + ctx.parent = depth === 0 ? null : parent ctx.depth = depth // Enter phase (offsets are positive) @@ -96,11 +104,13 @@ function walkImplementation( switch (result.kind) { case WalkKind.Continue: { + offsets[depth] = ~offset // Prepare for exit phase, same offset + if (node.nodes && node.nodes.length > 0) { - stack.push([node.nodes, 0, node as Parent]) + depth++ + offsets[depth] = 0 + parents[depth] = node as Parent } - - frame[1] = ~offset // Prepare for exit phase, same offset continue } @@ -108,7 +118,7 @@ function walkImplementation( return // Stop immediately case WalkKind.Skip: { - frame[1] = ~offset // Prepare for exit phase, same offset + offsets[depth] = ~offset // Prepare for exit phase, same offset continue } @@ -124,7 +134,7 @@ function walkImplementation( case WalkKind.ReplaceSkip: { nodes.splice(offset, 1, ...result.nodes) - frame[1] += result.nodes.length // Advance to next sibling past replacements + offsets[depth] += result.nodes.length // Advance to next sibling past replacements continue } @@ -146,7 +156,7 @@ function walkImplementation( switch (result.kind) { case WalkKind.Continue: - frame[1] = index + 1 // Advance to next sibling + offsets[depth] = index + 1 // Advance to next sibling continue case WalkKind.Stop: @@ -154,7 +164,7 @@ function walkImplementation( case WalkKind.Replace: { nodes.splice(index, 1, ...result.nodes) - frame[1] = index + result.nodes.length // Advance to next sibling past replacements + offsets[depth] = index + result.nodes.length // Advance to next sibling past replacements continue } @@ -165,7 +175,7 @@ function walkImplementation( case WalkKind.ReplaceSkip: { nodes.splice(index, 1, ...result.nodes) - frame[1] = index + result.nodes.length // Advance to next sibling past replacements + offsets[depth] = index + result.nodes.length // Advance to next sibling past replacements continue } From f7382fedbc72acc4d3a864144606bc1999a1529c Mon Sep 17 00:00:00 2001 From: Robin Malfait Date: Wed, 15 Oct 2025 21:34:45 +0200 Subject: [PATCH 2/2] mutate node outside of the walk Co-authored-by: Jordan Pittman --- packages/tailwindcss/src/variants.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/tailwindcss/src/variants.ts b/packages/tailwindcss/src/variants.ts index 82a2b8592cfe..24b72ce423e0 100644 --- a/packages/tailwindcss/src/variants.ts +++ b/packages/tailwindcss/src/variants.ts @@ -447,6 +447,7 @@ export function createVariants(theme: Theme): Variants { if (variant.modifier) return null let didApply = false + let replacement: AstNode | null = null walk([ruleNode], (node, ctx) => { if (node.kind !== 'rule' && node.kind !== 'at-rule') return WalkAction.Continue @@ -492,7 +493,7 @@ export function createVariants(theme: Theme): Variants { rules.push(negatedAtRule) } - Object.assign(ruleNode, styleRule('&', rules)) + replacement = styleRule('&', rules) // Track that the variant was actually applied didApply = true @@ -500,6 +501,10 @@ export function createVariants(theme: Theme): Variants { return WalkAction.Skip }) + if (replacement) { + Object.assign(ruleNode, replacement) + } + // TODO: Tweak group, peer, has to ignore intermediate `&` selectors (maybe?) if (ruleNode.kind === 'rule' && ruleNode.selector === '&' && ruleNode.nodes.length === 1) { Object.assign(ruleNode, ruleNode.nodes[0])