Speed up component tree resolution ($parent, $root, $query)#730
Merged
Conversation
…nstances The `__parent` getter (backing `$parent` and `$root`) iterated the whole global instance set on every access, plus a defensive `Set` copy and an `$el.contains()` walk per candidate — O(N_instances × depth). Walk the DOM ancestors and resolve via each element's `__base__` map instead (the same O(1)-per-ancestor lookup `closestComponent` already uses), guarded by an O(1) `hasInstance()` membership check to preserve the "mounted instance" semantics. Resolution is now O(tree depth), independent of instance count. Co-authored-by: Claude
Compares the previous global-scan strategy against the DOM ancestor walk across 50/500/2000 instances. The scan cost grows with instance count (~0.03ms → ~1.05ms) while the walk stays flat (~0.0004ms), regardless of N. Co-authored-by: Claude
…scan
`queryComponent`/`queryComponentAll` scanned the whole global instance set on
every call. Split resolution by scope: `from=document` keeps the global-storage
path (so detached elements and the no-containment semantics are preserved
byte-for-byte), while `from=element` resolves matches through a scoped
`querySelectorAll('*')` descendant traversal plus an explicit self check, reading
each element's `__base__` map (keyed on config.name only, attribute-independent),
gated by `hasInstance` + terminated-skip + `instanceIsMatching`. The element path
now scales with the `from` subtree size instead of the total instance count.
Two behavioral changes on the element-scoped path, both edge cases: results are
returned in DOM order rather than mount order, and when several live instances
share the same name on the same element only one is returned (the `__base__`
entry). Documented in the changelog.
Co-authored-by: Claude
Compares the global-scan strategy against the scoped DOM traversal for element-scoped queries across 50/500/2000 instances (~14x/174x/595x faster, independent of the total mounted-instance count). Co-authored-by: Claude
Co-authored-by: Claude
titouanmathis
force-pushed
the
perf/component-tree-resolution
branch
from
July 13, 2026 12:58
46ed51f to
d88fec2
Compare
Export Size@studiometa/js-toolkit
Unchanged@studiometa/js-toolkit
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #730 +/- ##
==========================================
- Coverage 97.78% 97.42% -0.37%
==========================================
Files 162 162
Lines 3710 3729 +19
Branches 1040 1040
==========================================
+ Hits 3628 3633 +5
- Misses 74 86 +12
- Partials 8 10 +2
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Merging this PR will improve performance by 31.25%
Performance Changes
Tip Curious why this is faster? Comment Comparing |
This was referenced Jul 13, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Component tree resolution scanned the whole global instance set on every access. This replaces those O(N_instances) scans with DOM-local lookups, keyed on each element's
__base__map — the same O(1)-per-ancestor patternclosestComponent/$closestalready uses.__parent(backing$parentand$root): resolve by walking the DOM ancestors instead of scanning every instance + copying the storageSet. Now O(tree depth), independent of instance count. Added an O(1)hasInstance()helper to preserve the exact "mounted instance" visibility semantics.queryComponent/queryComponentAll($query/$queryAll): split by scope.from=documentkeeps the global-storage path (so detached elements and the no-containment semantics are preserved byte-for-byte), whilefrom=elementresolves through a scopedquerySelectorAll('*')+ explicit self-check +__base__lookup, gated byhasInstance+ terminated-skip +instanceIsMatching. The element path now scales with thefromsubtree size, not the total instance count.Benchmarks
Added
parent-resolution.bench.tsandquery-resolution.bench.ts(50/500/2000 instances, depth 5). The scan cost grows with instance count while the new strategies stay flat:__parent(DOM walk vs global scan)$queryelement-scoped (scoped DOM vs global scan)Behavioral changes (documented in the changelog)
Two edge-case changes on the element-scoped
$query/$queryAllpath, both inherent to the__base__(one entry per name per element) approach:queryComponentmay return a different instance when several match within an element root.getInstanceFromElement; it requires manual double-construction).The normal one-instance-per-element case is verified identical to the previous behavior.
Tests
Full suite green (888 passed, 2 skipped). Added 5 tests covering the scoped-query invariants: detached elements with
from=document,from-self match, attribute-independent resolution,:mounted/:destroyedfilters, and skipping terminated__base__entries.oxlintandtsgoclean.🤖 Generated with Claude Code