Skip to content

Perf: slice TrackedArray to plain array before iterating (runtime)#25

Closed
johanrd wants to merge 1 commit intomainfrom
perf/iterable-slice-fast-path
Closed

Perf: slice TrackedArray to plain array before iterating (runtime)#25
johanrd wants to merge 1 commit intomainfrom
perf/iterable-slice-fast-path

Conversation

@johanrd
Copy link
Copy Markdown
Owner

@johanrd johanrd commented Apr 19, 2026

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's get trap (convertToInt + readStorageFor + consumeTag) — N Proxy traps per 1000-item iteration.

Call .slice() on the iterable before wrapping. For a TrackedArray, .slice goes through the Proxy's ARRAY_GETTER_METHODS handler which consumes the collection tag (preserving autotracking) and then invokes target.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

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.
@github-actions
Copy link
Copy Markdown

📊 Package size report   0.02%↑

File Before (Size / Brotli) After (Size / Brotli)
dist/dev/packages/@glimmer/reference/index.js 5.9 kB / 1.7 kB 7%↑6.3 kB / 11%↑1.9 kB
dist/prod/packages/@glimmer/reference/index.js 5.7 kB / 1.6 kB 8%↑6.1 kB / 11%↑1.8 kB
Total (Includes all files) 5.4 MB / 1.3 MB 0.02%↑5.4 MB / 0.03%↑1.3 MB
Tarball size 1.2 MB 0.03%↑1.2 MB

🤖 This report was automatically generated by pkg-size-action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant