From 10d962551b29ae370c0d20464242f963560ebde2 Mon Sep 17 00:00:00 2001 From: Mathias Picker Date: Fri, 27 Feb 2026 12:14:13 +0100 Subject: [PATCH 1/3] perf: use walk_readonly for analysis-phase AST walks Switch all read-only AST walks in the analysis phase, scope creation, CSS analysis/pruning/warnings, migration, and utility functions from zimmerframe's `walk()` to `walk_readonly()`. The readonly variant eliminates per-node mutation tracking overhead (no mutations object, no Object.keys checks, no apply_mutations) and avoids object spreads in the universal visitor path, yielding ~20% faster compile times. Depends on sveltejs/zimmerframe#33 Co-Authored-By: Claude Opus 4.6 --- packages/svelte/src/compiler/migrate/index.js | 12 ++++++------ .../phases/2-analyze/css/css-analyze.js | 12 ++++++------ .../compiler/phases/2-analyze/css/css-prune.js | 14 +++++++------- .../compiler/phases/2-analyze/css/css-warn.js | 8 ++++---- .../src/compiler/phases/2-analyze/index.js | 16 ++++++++-------- .../src/compiler/phases/2-analyze/types.d.ts | 14 ++++++-------- .../2-analyze/visitors/shared/a11y/index.js | 4 ++-- packages/svelte/src/compiler/phases/scope.js | 18 +++++++++--------- packages/svelte/src/compiler/utils/ast.js | 6 +++--- 9 files changed, 51 insertions(+), 53 deletions(-) diff --git a/packages/svelte/src/compiler/migrate/index.js b/packages/svelte/src/compiler/migrate/index.js index ce5387f4dd..9c4b0b9c12 100644 --- a/packages/svelte/src/compiler/migrate/index.js +++ b/packages/svelte/src/compiler/migrate/index.js @@ -1,10 +1,10 @@ /** @import { VariableDeclarator, Node, Identifier, AssignmentExpression, LabeledStatement, ExpressionStatement } from 'estree' */ -/** @import { Visitors } from 'zimmerframe' */ +/** @import { ReadonlyVisitors } from 'zimmerframe' */ /** @import { ComponentAnalysis } from '../phases/types.js' */ /** @import { Scope } from '../phases/scope.js' */ /** @import { AST, Binding, ValidatedCompileOptions } from '#compiler' */ import MagicString from 'magic-string'; -import { walk } from 'zimmerframe'; +import { walk_readonly } from 'zimmerframe'; import { parse } from '../phases/1-parse/index.js'; import { regex_valid_component_name } from '../phases/1-parse/state/element.js'; import { analyze_component } from '../phases/2-analyze/index.js'; @@ -213,11 +213,11 @@ export function migrate(source, { filename, use_ts } = {}) { } if (parsed.instance) { - walk(parsed.instance.content, state, instance_script); + walk_readonly(parsed.instance.content, state, instance_script); } state = { ...state, scope: analysis.template.scope }; - walk(parsed.fragment, state, template); + walk_readonly(parsed.fragment, state, template); let insertion_point = parsed.instance ? /** @type {number} */ (parsed.instance.content.start) @@ -481,7 +481,7 @@ export function migrate(source, { filename, use_ts } = {}) { * }} State */ -/** @type {Visitors} */ +/** @type {ReadonlyVisitors} */ const instance_script = { _(node, { state, next }) { // @ts-expect-error @@ -1052,7 +1052,7 @@ function trim_block(state, start, end) { } } -/** @type {Visitors} */ +/** @type {ReadonlyVisitors} */ const template = { Identifier(node, { state, path }) { handle_identifier(node, state, path); diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js index d6052c9c3e..c0b3d6f511 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-analyze.js @@ -1,7 +1,7 @@ /** @import { ComponentAnalysis } from '../../types.js' */ /** @import { AST } from '#compiler' */ -/** @import { Visitors } from 'zimmerframe' */ -import { walk } from 'zimmerframe'; +/** @import { ReadonlyVisitors } from 'zimmerframe' */ +import { walk_readonly } from 'zimmerframe'; import * as e from '../../../errors.js'; import { is_keyframes_node } from '../../css.js'; import { is_global, is_unscoped_pseudo_class } from './utils.js'; @@ -15,7 +15,7 @@ import { is_global, is_unscoped_pseudo_class } from './utils.js'; */ /** - * @typedef {Visitors} CssVisitors + * @typedef {ReadonlyVisitors} CssVisitors */ /** @@ -183,7 +183,7 @@ const css_visitors = { if (node.metadata.is_global_like || node.metadata.is_global) { // So that nested selectors like `:root:not(.x)` are not marked as unused for (const child of node.selectors) { - walk(/** @type {AST.CSS.Node} */ (child), null, { + walk_readonly(/** @type {AST.CSS.Node} */ (child), null, { ComplexSelector(node, context) { node.metadata.used = true; context.next(); @@ -222,7 +222,7 @@ const css_visitors = { node.metadata.is_global_block = is_global_block = true; for (let i = 1; i < child.selectors.length; i++) { - walk(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, { + walk_readonly(/** @type {AST.CSS.Node} */ (child.selectors[i]), null, { ComplexSelector(node) { node.metadata.used = true; } @@ -327,5 +327,5 @@ export function analyze_css(stylesheet, analysis) { analysis }; - walk(stylesheet, css_state, css_visitors); + walk_readonly(stylesheet, css_state, css_visitors); } diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js index 7e05d2e7d3..bac8b60364 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-prune.js @@ -1,5 +1,5 @@ /** @import * as Compiler from '#compiler' */ -import { walk } from 'zimmerframe'; +import { walk_readonly } from 'zimmerframe'; import { get_parent_rules, get_possible_values, @@ -128,7 +128,7 @@ const seen = new Set(); * @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement} element */ export function prune(stylesheet, element) { - walk(/** @type {Compiler.AST.CSS.Node} */ (stylesheet), null, { + walk_readonly(/** @type {Compiler.AST.CSS.Node} */ (stylesheet), null, { Rule(node, context) { if (node.metadata.is_global_block) { context.visit(node.prelude); @@ -174,7 +174,7 @@ function get_relative_selectors(node) { // nesting could be inside pseudo classes like :is, :has or :where for (let selector of selectors) { - walk(selector, null, { + walk_readonly(selector, null, { // @ts-ignore NestingSelector() { has_explicit_nesting_selector = true; @@ -493,7 +493,7 @@ function relative_selector_might_apply_to_node(relative_selector, rule, element, // with descendants, in which case we scope them all. if (name === 'not' && selector.args) { for (const complex_selector of selector.args.children) { - walk(complex_selector, null, { + walk_readonly(complex_selector, null, { ComplexSelector(node, context) { node.metadata.used = true; context.next(); @@ -828,7 +828,7 @@ function get_ancestor_elements(node, adjacent_only, seen = new Set()) { if (select_element && (!adjacent_only || is_direct_child)) { /** @type {Compiler.AST.RegularElement | null} */ let selectedcontent_element = null; - walk(select_element, null, { + walk_readonly(select_element, null, { RegularElement(child, context) { if (child.name === 'selectedcontent') { selectedcontent_element = child; @@ -871,7 +871,7 @@ function get_descendant_elements(node, adjacent_only, seen = new Set()) { * @param {Compiler.AST.SvelteNode} node */ function walk_children(node) { - walk(node, null, { + walk_readonly(node, null, { _(node, context) { if (node.type === 'RegularElement' || node.type === 'SvelteElement') { descendants.push(node); @@ -905,7 +905,7 @@ function get_descendant_elements(node, adjacent_only, seen = new Set()) { ); if (select_element) { - walk( + walk_readonly( select_element, { inside_option: false }, { diff --git a/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js b/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js index 238c83f00e..33a1689fc3 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js +++ b/packages/svelte/src/compiler/phases/2-analyze/css/css-warn.js @@ -1,6 +1,6 @@ -/** @import { Visitors } from 'zimmerframe' */ +/** @import { ReadonlyVisitors } from 'zimmerframe' */ /** @import { AST } from '#compiler' */ -import { walk } from 'zimmerframe'; +import { walk_readonly } from 'zimmerframe'; import * as w from '../../../warnings.js'; import { is_keyframes_node } from '../../css.js'; @@ -8,10 +8,10 @@ import { is_keyframes_node } from '../../css.js'; * @param {AST.CSS.StyleSheet} stylesheet */ export function warn_unused(stylesheet) { - walk(stylesheet, { stylesheet }, visitors); + walk_readonly(stylesheet, { stylesheet }, visitors); } -/** @type {Visitors} */ +/** @type {ReadonlyVisitors} */ const visitors = { Atrule(node, context) { if (!is_keyframes_node(node)) { diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index 969af842cc..67cf6d3b27 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -2,7 +2,7 @@ /** @import { Binding, AST, ValidatedCompileOptions, ValidatedModuleCompileOptions } from '#compiler' */ /** @import { AnalysisState, Visitors } from './types' */ /** @import { Analysis, ComponentAnalysis, Js, ReactiveStatement, Template } from '../types' */ -import { walk } from 'zimmerframe'; +import { walk_readonly } from 'zimmerframe'; import { parse } from '../1-parse/acorn.js'; import * as e from '../../errors.js'; import * as w from '../../warnings.js'; @@ -295,7 +295,7 @@ export function analyze_module(source, options) { runes: true }); - walk( + walk_readonly( /** @type {ESTree.Node} */ (ast), { scope, @@ -637,7 +637,7 @@ export function analyze_component(root, source, options) { // more legacy nonsense: if an `each` binding is reassigned/mutated, // treat the expression as being mutated as well - walk(/** @type {AST.SvelteNode} */ (template.ast), null, { + walk_readonly(/** @type {AST.SvelteNode} */ (template.ast), null, { EachBlock(node) { const scope = /** @type {Scope} */ (template.scopes.get(node)); @@ -645,7 +645,7 @@ export function analyze_component(root, source, options) { if (binding.updated) { const state = { scope: /** @type {Scope} */ (scope.parent), scopes: template.scopes }; - walk(node.expression, state, { + walk_readonly(node.expression, state, { // @ts-expect-error _: set_scope, Identifier(node, context) { @@ -722,7 +722,7 @@ export function analyze_component(root, source, options) { derived_function_depth: -1 }; - walk(/** @type {AST.SvelteNode} */ (ast), state, visitors); + walk_readonly(/** @type {AST.SvelteNode} */ (ast), state, visitors); } // warn on any nonstate declarations that are a) reassigned and b) referenced in the template @@ -790,7 +790,7 @@ export function analyze_component(root, source, options) { derived_function_depth: -1 }; - walk(/** @type {AST.SvelteNode} */ (ast), state, visitors); + walk_readonly(/** @type {AST.SvelteNode} */ (ast), state, visitors); } for (const [name, binding] of instance.scope.declarations) { @@ -954,7 +954,7 @@ function calculate_blockers(instance, analysis) { if (seen.has(expression)) return; seen.add(expression); - walk( + walk_readonly( expression, { scope }, { @@ -1007,7 +1007,7 @@ function calculate_blockers(instance, analysis) { } } - walk( + walk_readonly( node, { scope }, { diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index 9d24f9dbac..9f18911709 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -35,12 +35,10 @@ export interface AnalysisState { derived_function_depth: number; } -export type Context = import('zimmerframe').Context< - AST.SvelteNode, - State ->; +export type Context< + State extends AnalysisState = AnalysisState +> = import('zimmerframe').ReadonlyContext; -export type Visitors = import('zimmerframe').Visitors< - AST.SvelteNode, - State ->; +export type Visitors< + State extends AnalysisState = AnalysisState +> = import('zimmerframe').ReadonlyVisitors; diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js index 45de8b10a1..6c3d20fde1 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/shared/a11y/index.js @@ -44,7 +44,7 @@ import { } from '../../../../patterns.js'; import { is_event_attribute, is_text_attribute } from '../../../../../utils/ast.js'; import { list } from '../../../../../utils/string.js'; -import { walk } from 'zimmerframe'; +import { walk_readonly } from 'zimmerframe'; import fuzzymatch from '../../../../1-parse/utils/fuzzymatch.js'; import { is_content_editable_binding } from '../../../../../../utils.js'; import * as w from '../../../../../warnings.js'; @@ -456,7 +456,7 @@ export function check_element(node, context) { /** @param {AST.TemplateNode} node */ const has_input_child = (node) => { let has = false; - walk( + walk_readonly( node, {}, { diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 9d563375d7..520d7a9c55 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -1,8 +1,8 @@ /** @import { BinaryOperator, ClassDeclaration, Expression, FunctionDeclaration, Identifier, ImportDeclaration, MemberExpression, LogicalOperator, Node, Pattern, UnaryOperator, VariableDeclarator, Super, SimpleLiteral, FunctionExpression, ArrowFunctionExpression } from 'estree' */ -/** @import { Context, Visitor } from 'zimmerframe' */ +/** @import { ReadonlyContext, ReadonlyVisitor } from 'zimmerframe' */ /** @import { AST, BindingKind, DeclarationKind } from '#compiler' */ import is_reference from 'is-reference'; -import { walk } from 'zimmerframe'; +import { walk_readonly } from 'zimmerframe'; import { ExpressionMetadata } from './nodes.js'; import * as b from '#compiler/builders'; import * as e from '../errors.js'; @@ -915,7 +915,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { } /** - * @type {Visitor} + * @type {ReadonlyVisitor} */ const create_block_scope = (node, { state, next }) => { const scope = state.scope.child(true); @@ -925,7 +925,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { }; /** - * @type {Visitor} + * @type {ReadonlyVisitor} */ const SvelteFragment = (node, { state, next }) => { const scope = state.scope.child(); @@ -934,7 +934,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { }; /** - * @type {Visitor} + * @type {ReadonlyVisitor} */ const Component = (node, context) => { node.metadata.scopes = { @@ -975,7 +975,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { }; /** - * @type {Visitor} + * @type {ReadonlyVisitor} */ const SvelteDirective = (node, { state, path, visit }) => { state.scope.reference(b.id(node.name.split('.')[0]), path); @@ -987,7 +987,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { let has_await = false; - walk(ast, state, { + walk_readonly(ast, state, { AwaitExpression(node, context) { // this doesn't _really_ belong here, but it allows us to // automatically opt into runes mode on encountering @@ -1203,7 +1203,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { let inside_rest = false; let is_rest_id = false; - walk(node.context, null, { + walk_readonly(node.context, null, { Identifier(node) { if (inside_rest && node === id) { is_rest_id = true; @@ -1376,7 +1376,7 @@ export function create_scopes(ast, root, allow_reactive_declarations, parent) { /** * @template {{ scope: Scope, scopes: Map }} State * @param {AST.SvelteNode} node - * @param {Context} context + * @param {ReadonlyContext} context */ export function set_scope(node, { next, state }) { const scope = state.scopes.get(node); diff --git a/packages/svelte/src/compiler/utils/ast.js b/packages/svelte/src/compiler/utils/ast.js index 75aadd905b..8ffb81c71a 100644 --- a/packages/svelte/src/compiler/utils/ast.js +++ b/packages/svelte/src/compiler/utils/ast.js @@ -1,6 +1,6 @@ /** @import { AST, Scope } from '#compiler' */ /** @import * as ESTree from 'estree' */ -import { walk } from 'zimmerframe'; +import { walk_readonly } from 'zimmerframe'; import * as b from '#compiler/builders'; /** @@ -149,7 +149,7 @@ export function extract_all_identifiers_from_expression(expr) { /** @type {string[]} */ let keypath = []; - walk( + walk_readonly( expr, {}, { @@ -616,7 +616,7 @@ export function build_assignment_value(operator, left, right) { export function has_await_expression(node) { let has_await = false; - walk(node, null, { + walk_readonly(node, null, { AwaitExpression(_node, context) { has_await = true; context.stop(); From 079d473e3cf6510c054b46e150f5981bb3c27903 Mon Sep 17 00:00:00 2001 From: Mathias Picker Date: Fri, 27 Feb 2026 12:22:28 +0100 Subject: [PATCH 2/3] fix: prettier formatting in types.d.ts Co-Authored-By: Claude Opus 4.6 --- .../svelte/src/compiler/phases/2-analyze/types.d.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts index 9f18911709..b4130c8424 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/types.d.ts +++ b/packages/svelte/src/compiler/phases/2-analyze/types.d.ts @@ -35,10 +35,8 @@ export interface AnalysisState { derived_function_depth: number; } -export type Context< - State extends AnalysisState = AnalysisState -> = import('zimmerframe').ReadonlyContext; +export type Context = + import('zimmerframe').ReadonlyContext; -export type Visitors< - State extends AnalysisState = AnalysisState -> = import('zimmerframe').ReadonlyVisitors; +export type Visitors = + import('zimmerframe').ReadonlyVisitors; From 78c0bf94b86b466a42286c9dd0395a964e1af006 Mon Sep 17 00:00:00 2001 From: Mathias Picker <48158184+MathiasWP@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:34:50 +0100 Subject: [PATCH 3/3] Update changeset for fast-ducks-drop feature Updated the changeset for the 'fast-ducks-drop' feature to include performance improvements. --- .changeset/fast-ducks-drop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fast-ducks-drop.md diff --git a/.changeset/fast-ducks-drop.md b/.changeset/fast-ducks-drop.md new file mode 100644 index 0000000000..7622b8e92a --- /dev/null +++ b/.changeset/fast-ducks-drop.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +perf: use walk_readonly for analysis-phase AST walks