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;