Skip to content

Make the functions of a TrackedValue on their first use - #21666

Merged
NullVoxPopuli merged 3 commits into
emberjs:mainfrom
NullVoxPopuli-ai-agent:nvp/tracked-value-lazy-functions
Oct 7, 2026
Merged

NullVoxPopuli merged 3 commits into
emberjs:mainfrom
NullVoxPopuli-ai-agent:nvp/tracked-value-lazy-functions

Conversation

@NullVoxPopuli-ai-agent

@NullVoxPopuli-ai-agent NullVoxPopuli-ai-agent commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

tracked(value) allocates less. Each TrackedValue made four arrow functions and one options object. Now it makes a function only when code reads it.

This is a part of #21650, and it merges alone. Only tracked-value.ts and its test change.

case main this PR
create: 1000 signals 17.84 µs 9.39 µs (0.5x)
create: 1000 computeds, read each 41.11 µs 40.06 µs (1.0x)
rows: 1000 rows, write all 118.18 µs 111.87 µs (0.9x)
weighted geometric mean 1.0x 0.9x

The gain is in the create of a tracked value. Reads and writes do not change.

What changes

  • get, set, update and freeze are accessors that make the bound function on its first read, and keep it.
  • Each accessor has a setter, so code can still assign to them. A write through value uses an assigned set, as on main.
  • The default options are one frozen object for all instances.

What changes for callers

  • The four functions are still bound, and each read gives the same function. The test "methods can be detached from the instance" from RFC 1071 still passes.
  • They are now accessors on the prototype, so Object.keys() and spread do not list them.
All 20 cases
case main this PR
propagate: 1 chains x 1 deep 124 ns 122 ns (1.0x)
propagate: 10 chains x 10 deep 5.51 µs 5.49 µs (1.0x)
propagate: 100 chains x 100 deep 724.68 µs 713.81 µs (1.0x)
propagate: 1 chains x 1000 deep 57.18 µs 57.57 µs (1.0x)
propagate: 1000 chains x 1 deep 103.01 µs 100.65 µs (1.0x)
kairo: avoidable propagation 382 ns 374 ns (1.0x)
kairo: broad propagation 7.35 µs 7.25 µs (1.0x)
kairo: deep propagation 3.09 µs 3.04 µs (1.0x)
kairo: diamond 428 ns 422 ns (1.0x)
kairo: mux 20.23 µs 20.17 µs (1.0x)
kairo: repeated observers 365 ns 332 ns (0.9x)
kairo: triangle 755 ns 749 ns (1.0x)
kairo: unstable 574 ns 577 ns (1.0x)
rows: 1000 rows, write 1 7.19 µs 7.09 µs (1.0x)
rows: 1000 rows, write all 118.18 µs 111.87 µs (0.9x)
batch: 10 writes, 1 output 667 ns 634 ns (1.0x)
avoidable: write the same value 6 ns 6 ns (1.1x)
create: 1000 signals 17.84 µs 9.39 µs (0.5x)
create: 1000 computeds, read each 41.11 µs 40.06 µs (1.0x)
create: 1000 outputs 50.30 µs 52.33 µs (1.0x)
weighted geometric mean 1.0x 0.9x

The benchmark is https://github.com/NullVoxPopuli-ai-agent/ember-reactivity-bench. One measurement is the writes of one frame, then one flush that brings every output up to date. Each case of each build runs in its own process, pinned to one core. The numbers are the median of 6 mirrored rounds on Node 24.20.

The run has main, each of the five branches, and #21650 as columns: table and method.

Tests
  • The full suite passes locally: 9498 pass, 18 skipped, 0 failed.
  • type-check:internals, ESLint and Prettier pass.
  • New tests: each function is the same on each read, each function can be replaced, and a detached freeze() prevents updates.

The parts of #21650

#21650 has all five changes in one branch. It stays open as the reference for the combined numbers. The five branches merge with no conflict, and their merge is the same code as the head of #21650.

PR change reactivity benchmark, mean against main
#21663 Pool the trackers of tracking frames 0.6x
#21664 Reuse the combined tag of a tracking frame. Needs #21663 0.6x, with the pool
#21665 Index loop over the subtags of a combined tag 1.0x
#21666 (this PR) Make the functions of a TrackedValue on their first use 0.9x, create is 0.5x
#21667 Store 0 first in the value field of a TrackedValue 1.0x, removes one deopt
#21650 all five 0.6x

Rendering was measured for the combined change only: pnpm bench shows 2.1% less script time, in this comment on #21650.

🤖 Generated with Claude Code

Each TrackedValue made four arrow functions and one options object.
`get`, `set`, `update` and `freeze` are now accessors that make the
bound function on its first read, and keep it. Each one has a setter,
so code can still assign to them.

The default options are one frozen object.

Split out of emberjs#21650.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Removed setter methods for get, set, update, and freeze in TrackedValue.
ef4
ef4 previously approved these changes Oct 6, 2026
@NullVoxPopuli
NullVoxPopuli merged commit d4217d5 into emberjs:main Oct 7, 2026
68 checks passed
@NullVoxPopuli
NullVoxPopuli deleted the nvp/tracked-value-lazy-functions branch October 7, 2026 15:36
@github-actions github-actions Bot mentioned this pull request Oct 7, 2026
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.

3 participants