Perf: slice TrackedArray to plain array before iterating (runtime)#25
Closed
Perf: slice TrackedArray to plain array before iterating (runtime)#25
Conversation
When a Glimmer {{#each}} iterates a TrackedArray, every per-item access
in ArrayIterator.next() goes through the Proxy `get` trap, which calls
convertToInt + readStorageFor + consumeTag on each numeric-index read.
For a 5000-row list that's 5000 Proxy traps per iteration pass.
Call .slice() on the Proxy up front. Two important things happen:
- The Proxy's ARRAY_GETTER_METHODS handler for `.slice` consumes the
collection tag inside the enclosing iterator-ref track frame, so
autotracking is preserved (the ref still invalidates when the array
mutates).
- The .slice() call itself uses `target.slice(...)` where target is the
underlying plain array, so it's a fast internal V8 copy that does not
trigger per-element Proxy traps.
Subsequent iteration through ArrayIterator reads the plain array with
no Proxy overhead.
Plain (non-Proxy) arrays also go through this path. The extra O(N) copy
is cheap (V8's slice is highly optimized) and there's no obvious way to
distinguish a Proxy from its target without adding a cross-package
dependency on @glimmer/validator. Benchmark data will decide whether
the plain-array tax is worth the TrackedArray win.
📊 Package size report 0.02%↑
🤖 This report was automatically generated by pkg-size-action |
2 tasks
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.
Draft — opened to run CI tests before benching. Will promote to ready or close based on results.
Extract of NVP's upstream emberjs#21221. On Glimmer's
{{#each}}iterating a TrackedArray,ArrayIterator.next()reads each item through the Proxy'sgettrap (convertToInt + readStorageFor + consumeTag) — N Proxy traps per 1000-item iteration.Call
.slice()on the iterable before wrapping. For a TrackedArray,.slicegoes through the Proxy's ARRAY_GETTER_METHODS handler which consumes the collection tag (preserving autotracking) and then invokestarget.slice(...)on the underlying plain array. Subsequent iteration reads the plain array with zero Proxy overhead. For non-Proxy arrays, it's an unconditional O(N) copy that the benchmark data will decide is worth it.Test plan
{{each}} works when updating old itemscaught a reactivity break in Perf: flatten ArrayIterator state machine to a boolean (runtime) #24)pnpm benchshows improvement without the "broken reactivity" signature (no suspicious single-phase drops >30%, no paired-asymmetric regressions)