diff --git a/.changeset/tidy-melons-attack.md b/.changeset/tidy-melons-attack.md new file mode 100644 index 0000000000..87e62a1c4d --- /dev/null +++ b/.changeset/tidy-melons-attack.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +perf: make async blocker analysis scale linearly with the number of top-level references diff --git a/packages/svelte/src/compiler/phases/2-analyze/index.js b/packages/svelte/src/compiler/phases/2-analyze/index.js index d81053a08d..f9cf2f1d89 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/index.js +++ b/packages/svelte/src/compiler/phases/2-analyze/index.js @@ -958,7 +958,7 @@ function calculate_blockers(instance, analysis) { * @param {Set} touched * @param {Set} seen */ - const touch = (expression, scope, touched, seen = new Set()) => { + const touch = (expression, scope, touched, seen) => { if (seen.has(expression)) return; seen.add(expression); @@ -1015,6 +1015,13 @@ function calculate_blockers(instance, analysis) { } } + // Share seen nodes across calls so transitive assignments are only visited once. + // Keep separate read/write state because the target sets can differ. + /** @type {Set} */ + const writes_seen = new Set(); + /** @type {Set} */ + const reads_seen = new Set(); + walk( node, { scope }, @@ -1044,13 +1051,7 @@ function calculate_blockers(instance, analysis) { const rune = get_rune(node, context.state.scope); if (rune === '$effect') return; - /** @type {Set} */ - const touched = new Set(); - touch(node, context.state.scope, touched); - - for (const b of touched) { - writes.add(b); - } + touch(node, context.state.scope, writes, writes_seen); }, Identifier(node, context) { const parent = /** @type {ESTree.Node} */ (context.path.at(-1)); @@ -1066,7 +1067,7 @@ function calculate_blockers(instance, analysis) { // might be called immediately, so we have to touch all references within it. Example: // function foo() { return () => blocker; } foo(); // blocker is touched if (node.argument) { - touch(node.argument, context.state.scope, reads); + touch(node.argument, context.state.scope, reads, reads_seen); } }, // don't look inside functions until they are called @@ -1242,7 +1243,7 @@ function calculate_blockers(instance, analysis) { } else { // A concise arrow body is an implicit return, so treat it like the // `ReturnStatement` visitor in `trace_references` would. - touch(init.body, fn_scope, reads_writes); + touch(init.body, fn_scope, reads_writes, new Set()); } const max = [...reads_writes].reduce((max, binding) => {