From 32111f9e847a5bfb33bdd9a125c368cbaf1580b1 Mon Sep 17 00:00:00 2001 From: Mathias Picker <48158184+MathiasWP@users.noreply.github.com> Date: Tue, 3 Mar 2026 14:38:26 +0100 Subject: [PATCH] =?UTF-8?q?perf:=20avoid=20O(n=C2=B2)=20name=20scanning=20?= =?UTF-8?q?in=20scope=20generate/unique=20(#17844)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `Scope.generate()` and `ScopeRoot.unique()` search for available names by iterating from suffix `_1` upward. When the same preferred name is generated many times (e.g. `text` is generated 482 times in a large component), the Nth call re-scans all N-1 already-taken names — O(n²) total work. This adds a `#name_counters` Map to `ScopeRoot` that tracks the next suffix to try per name, so each call resumes from where the last one left off. Generated names are identical to before. ## Benchmark (interleaved, best-of-3 rounds) | Component | Min | Median | |---|---|---| | Realistic (~80 lines) | ~1% | ~7% | | Medium (316 lines) | ~1% | ~5% | | Large (642 lines) | ~7% | ~2% | | XLarge (1302 lines) | **~11%** | **~10%** | ## Test plan - [x] All snapshot tests pass (name generation unchanged) - [x] All validator, compiler-error, runtime-runes, runtime-legacy tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.6 --- .changeset/smooth-pens-jump.md | 5 ++ packages/svelte/src/compiler/phases/scope.js | 50 ++++++++++++++++++-- 2 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 .changeset/smooth-pens-jump.md diff --git a/.changeset/smooth-pens-jump.md b/.changeset/smooth-pens-jump.md new file mode 100644 index 0000000000..79aea8e6e1 --- /dev/null +++ b/.changeset/smooth-pens-jump.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +perf: avoid O(n²) name scanning in scope `generate` and `unique` diff --git a/packages/svelte/src/compiler/phases/scope.js b/packages/svelte/src/compiler/phases/scope.js index 9d563375d7..e3560753b4 100644 --- a/packages/svelte/src/compiler/phases/scope.js +++ b/packages/svelte/src/compiler/phases/scope.js @@ -713,8 +713,18 @@ export class Scope { } preferred_name = preferred_name.replace(/[^a-zA-Z0-9_$]/g, '_').replace(/^[0-9]/, '_'); - let name = preferred_name; - let n = 1; + + // Use cached counter to skip names already known to be taken (avoids O(n²) scanning) + let n = this.root.next_counter(preferred_name); + let name; + + if (n === 0) { + name = preferred_name; + n = 1; + } else { + name = `${preferred_name}_${n}`; + n++; + } while ( this.references.has(name) || @@ -725,6 +735,7 @@ export class Scope { name = `${preferred_name}_${n++}`; } + this.root.set_counter(preferred_name, n); this.references.set(name, []); this.root.conflicts.add(name); return name; @@ -852,18 +863,49 @@ export class ScopeRoot { /** @type {Set} */ conflicts = new Set(); + /** + * Tracks the next suffix counter per name to avoid O(n) rescanning in generate/unique. + * @type {Map} + */ + #name_counters = new Map(); + + /** + * @param {string} name + * @returns {number} + */ + next_counter(name) { + return this.#name_counters.get(name) ?? 0; + } + + /** + * @param {string} name + * @param {number} value + */ + set_counter(name, value) { + this.#name_counters.set(name, value); + } + /** * @param {string} preferred_name */ unique(preferred_name) { preferred_name = preferred_name.replace(/[^a-zA-Z0-9_$]/g, '_'); - let final_name = preferred_name; - let n = 1; + let n = this.#name_counters.get(preferred_name) ?? 0; + let final_name; + + if (n === 0) { + final_name = preferred_name; + n = 1; + } else { + final_name = `${preferred_name}_${n}`; + n++; + } while (this.conflicts.has(final_name)) { final_name = `${preferred_name}_${n++}`; } + this.#name_counters.set(preferred_name, n); this.conflicts.add(final_name); const id = b.id(final_name); return id;