Skip to content

Feat(reactivity): ReactiveObject Improve Keyed Path Perf#288

Merged
jlukic merged 23 commits into
mainfrom
feat/keyed-write-canonicalization
Jul 2, 2026
Merged

Feat(reactivity): ReactiveObject Improve Keyed Path Perf#288
jlukic merged 23 commits into
mainfrom
feat/keyed-write-canonicalization

Conversation

@jlukic

@jlukic jlukic commented Jul 2, 2026

Copy link
Copy Markdown
Member

This fixes keys in ReactiveObject not waking from index updates when tracking an #id prop. This adds a helper to get the keyed path from an index string.

  • keyedPath(obj, path) in utils- Returns foo[#bar] from foo.0
  • ReactiveObject.set/remove - Add use of keyedPath to resolve canonical path.

@github-actions github-actions Bot added Reactivity Modifies reactivity package Tests Modifies tests Utils Modifies utilities package labels Jul 2, 2026
@semantic-bundle-bot

semantic-bundle-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown

🟡 Bundle size warning: component +137 B brotli · +88 shipped LOC for 2db4344

Base: main · Run: #28611988204 · Raw: size-report.json

Feat(reactivity): ReactiveObject Improve Keyed Path Perf

Warning

component grew +137 B brotli to 48.6 KB (+0.3%) across +88 shipped LOC. Largest increase: reactivity +363 B (+5.3%).

3 larger · 0 smaller · 21 unchanged · +88 shipped LOC · -23 comment LOC

signal result
component brotli +137 B (+0.3%)
Shipped LOC +88
Comment LOC -23
Changed bundles 3 / 24

Bundles that changed (3)

bundle brotli Δ brotli change
reactivity 7.1 KB +363 B +5.3%
component 🎯 48.6 KB +137 B +0.3%
utils 17.6 KB +290 B +1.6%

Sorted by absolute brotli delta, increases first. 🎯 = bundle most relevant to this PR. † = consumed piecemeal, so the whole-package bundle is an upper bound and does not drive the verdict.

LOC by scope: +88 shipped LOC · -23 comment LOC

Shipped LOC excludes comments and blank lines.

scope shipped LOC comment LOC
utils +79 +6
reactivity +9 -29
All 24 bundles, gzip, and raw sizes
bundle group brotli Δ brotli gzip Δ gzip raw Δ raw
compiler package 6.1 KB 6.8 KB 18.4 KB
component 🎯 package 48.6 KB +137 B 55.6 KB +94 B 169.8 KB +187 B
query package 16.0 KB +51 B 17.8 KB +55 B 52.5 KB +163 B
reactivity package 7.1 KB +363 B 7.8 KB +381 B 22.2 KB +1.17 KB
renderer package 43.2 KB +43 B 49.1 KB +102 B 150.8 KB +187 B
specs package 53.9 KB +83 B 63.8 KB +56 B 236.3 KB +161 B
tailwind package 67.4 KB 85.3 KB 335.3 KB
templating package 28.8 KB +34 B 32.3 KB +106 B 95.9 KB +187 B
utils package 17.6 KB +290 B 19.7 KB +349 B 50.9 KB +973 B
framework framework 129.7 KB -79 B 165.7 KB +88 B 697.4 KB +187 B
button primitive 12.0 KB +42 B 15.2 KB +58 B 128.6 KB +110 B
card primitive 3.0 KB 3.5 KB 15.8 KB
container primitive 694 B 888 B 2.4 KB
divider primitive 1.6 KB 1.9 KB 6.5 KB
icon primitive 26.3 KB 35.0 KB 143.7 KB
image primitive 776 B 993 B 2.2 KB
input primitive 3.9 KB 4.5 KB 15.6 KB
label primitive 1.3 KB 1.6 KB 5.5 KB
menu primitive 14.2 KB -2 B 16.3 KB -1 B 64.5 KB
modal primitive 1.7 KB 2.0 KB 6.7 KB
rail primitive 534 B 695 B 1.5 KB
segment primitive 3.1 KB 3.7 KB 19.7 KB
spinner primitive 1.8 KB 2.1 KB 8.0 KB
table primitive 555 B 692 B 1.3 KB

brotli q11 · gzip l9 · vs main · fresh build both sides · 57s

@semantic-heap-bot

semantic-heap-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown

⚪ No leak for 2db4344 on Heap Analysis 🧠

Base: main · Run: #28611988308 · Raw: memory-report.json

Feat(reactivity): ReactiveObject Improve Keyed Path Perf

Note

all teardown invariants held; footprint within noise.

✅ 0 broken · ✅ 6 held · 📊 footprint within noise

invariant baseline after 7 cycles verdict
live `Reaction` 0 0 ✅ held
live `ReactionScope` 0 0 ✅ held
live `DynamicRegion` 0 0 ✅ held
live `Dependency` 2 2 ✅ held
live `Signal` 2 2 ✅ held
detached DOM nodes 0 0 ✅ held

🎯 = the leak this PR introduced or fixed. counts are exact — a residual is a real leak, not a sample.

Footprint by scene (post-GC retained heap)
scene retained Δ
create-1k 4.9 MB +4 KB within noise
create-5k 14.4 MB 0 KB within noise
Reactivity micro (Node, --expose-gc)

10000 signals + computeds + reactions: 13.8 MB graph heapUsed (0 KB, 0%, within noise)

residual after teardown + GC: 10.9 MB — a footprint trend, not a gate.


7 cycles · GC ×2/sample · Chrome 148.0.7778.96 (pinned) · counts exact · heap ±4% floor · 1m22s

@semantic-performance-bot

semantic-performance-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown

⚪ No Meaningful Change for 2db4344 on Benchmark Suite 📊

Base: main · Action: #28611988273 · Raw: bench-report.json

Feat(reactivity): ReactiveObject Improve Keyed Path Perf

Note

This PR did not move any measured metrics.

✅ 0 faster · ❌ 0 slower · 🔍 38 unsure · ⚪ 37 no change · 📜 1 reopened


📜 Regressions from peak (1)

These metrics were faster on an earlier push to this PR. The most recent candidate is usually where to look.

metric regression prior peak likely candidates
mutate-grid-row-edit-600 7% baf74d3
⚪ No Change (37)

Metrics where this PR measured within ±2% of main — no meaningful performance change detected.

metric Change
template:active-indicator-200 -1.7% – +2.0%
ast-walk-15k -0.3% – +1.6%
build-html-string-10k -0.6% – +0.9%
todo:bulk-add-500 -1.3% – +0.9%
krausest:clear-10k -1.5% – +1.8%
computed-chain-10x60k -1.8% – +0.6%
computed-unobserved-200x500 -0.9% – +1.5%
krausest:create-10k -0.5% – +0.4%
krausest:create-1k -0.9% – +1.1%
dom-walker-1000x15 -1.1% – +1.9%
each-100 -1.1% – +1.0%
each-100-mount -0.6% – +0.8%
todo:edit-start-10 -0.4% – +1.9%
todo:filter-cycle-20 -0.7% – +0.9%
flush-fanout-allocation-1000x500 -0.6% – +0.2%
helper-100-mount -1.5% – +0.7%
mutate-doc-nested-200k +0.1% – +1.4%
parse-cold-complex-200 -1.1% – +1.5%
reaction-coalesce-400x100 -1.6% – +0.6%
reaction-dep-diff-45k -1.4% – +1.2%
reaction-flush-noop-5m -0.9% – +0.0%
reactive-fanout-500x1200 -1.5% – +0.8%
reactive-list-replace-1000x1000 -0.3% – +0.7%
reactive-multi-read-5x160k -0.7% – +0.3%
reactive-object-field-write-40k -0.6% – +1.5%
reactive-object-replace-15k -1.2% – +1.6%
reactive-push-2000x20 -1.4% – +1.2%
reactive-stable-deps-3reads-5000x100 -0.4% – +0.5%
reactive-stable-fanout-5000x100 -0.3% – +0.6%
snippet-args-5k -1.3% – +1.2%
template:subtemplate-data-blob-100 -0.6% – +0.9%
template:subtemplate-helpers-heavy-100x500 -1.2% – +0.8%
template:subtemplate-helpers-light-100x500 -1.3% – +1.6%
template:subtemplate-reactive-data-100x500 -1.2% – +0.8%
template:subtemplate-shorthand-props-100x500 -1.9% – +1.1%
todo:toggle-all-200 +0.3% – +1.6%
todo:toggle-middle-100 -0.3% – +0.9%
🔍 Unsure (38)

Too Fast to Measure Precisely (38)

On benches this short, OS jitter, GC, and JIT pauses drown out anything under 4%. Bigger changes than that still show up.

metric Change Test Time Expected Noise
template:active-indicator-nested-200 -2.1% – +4.1% ~21ms ±10%
todo:add-20 -0.9% – +2.6% ~11ms ±5%
krausest:append-1k -3.6% – +2.9% ~88ms ±8%
todo:clear-completed-250 -2.0% – +4.7% ~41ms ±8%
computed-subscribe-unsubscribe-10k -3.0% – +2.1% ~15ms ±7%
template:each-mount-1000 -1.3% – +2.1% ~53ms ±4%
todo:edit-cycle-5 -3.0% – -0.2% ~61ms ±3%
expr-js-10k -0.7% – +2.0% ~16ms ±6%
expr-lisp-50k -2.3% – +0.4% ~35ms ±6%
expr-simple-100k -2.7% – +0.3% ~22ms ±7%
helper-100-state-change-1k -2.8% – +1.1% ~4ms ±8%
mutate-grid-row-edit-600 +0.5% – +7.6% ~3ms ±9%
parse-cold-normal-500 -2.4% – +1.1% ~50ms ±8%
reactive-list-filter-1000x300 -2.8% – +2.7% ~6ms ±7%
reactive-set-index-300 -3.6% – +11.5% ~2ms ±20%
reactive-set-property-by-id-200 +0.6% – +11.1% ~3ms ±13%
todo:remove-50-back -1.0% – +2.4% ~6ms ±4%
todo:remove-50-front -1.4% – +3.3% ~7ms ±6%
todo:remove-50-middle -2.8% – +3.8% ~7ms ±8%
todo:remove-first-100 -3.7% – +2.3% ~13ms ±6%
todo:remove-last-100 -3.1% – +0.8% ~12ms ±3%
todo:remove-middle-100 -6.4% – +4.5% ~12ms ±13%
krausest:remove-row-back-100 -3.0% – +1.9% ~26ms ±6%
krausest:remove-row-front-20 -5.4% – +1.6% ~9ms ±9%
krausest:remove-row-middle-20 -6.0% – +1.0% ~6ms ±8%
todo:rename-500 -4.0% – +4.9% ~17ms ±11%
krausest:replace-1k -2.3% – +1.4% ~92ms ±5%
krausest:select-40 -4.3% – +3.7% ~6ms ±10%
set-same-10m -2.1% – +5.8% ~18ms ±10%
template:snippet-args-per-key-100x500 -2.2% – +0.5% ~33ms ±4%
template:snippet-in-subtemplate-100x1k -3.5% – +0.5% ~23ms ±7%
template:stable-ref-mutate-500 -5.7% – +0.3% ~14ms ±10%
sub-unsub-100k -1.5% – +2.9% ~29ms ±6%
krausest:swap-rows-20 -0.3% – +3.6% ~13ms ±4%
todo:toggle-100 -4.5% – +3.2% ~14ms ±9%
todo:toggle-first-100 -2.0% – +3.6% ~13ms ±7%
todo:toggle-last-100 -1.3% – +4.4% ~13ms ±7%
krausest:update-10th-50 -1.4% – +3.1% ~27ms ±6%
📖 Bench glossary (40 metrics)
metric what it tests
krausest:append-1k Appends 1000 new rows onto an existing 1000-row table.
krausest:clear-10k Clears a 10000-row table back to empty in a single operation.
krausest:create-10k Renders a fresh 10000-row table into an empty parent at ten times the create-1k scale.
krausest:create-1k Renders a fresh 1000-row table into an empty parent.
krausest:remove-row-back-100 Removes the last row 100 times from a 1000-row table, with no other rows needing to move.
krausest:remove-row-front-20 Removes the first row 20 times from a 1000-row table, with all remaining rows sliding up each time.
krausest:remove-row-middle-20 Removes the middle row 20 times from a 1000-row table, with the rows below it sliding up each time.
krausest:replace-1k Replaces 1000 rows with a fresh 1000-row set, diffing the keyed list against a populated table.
krausest:select-40 Highlights one row at a time across 40 rows so only the previous and newly highlighted rows update.
krausest:swap-rows-20 Swaps the second and second-to-last rows in a 1000-row table, repeated 20 times.
krausest:update-10th-50 Updates the label on every tenth row of a 1000-row table, looped 50 times to lift the work above noise.
template:active-indicator-200 Cycles selectedId across 200 list items. Only the previously and newly active items update their class.
template:active-indicator-nested-200 Cycles currentUrl through 50 leaf urls in a 5×10×4 nav. Only the previously and newly active leaves should update their…
template:each-mount-1000 Mounts a fresh 1000-item each block with five-field items so per-record allocation cost dominates the wall clock.
template:snippet-args-per-key-100x500 Mutates one snippet arg's source across 100 invocations, 500 cycles. Adjacent no-signal expressions stay quiet.
template:snippet-in-subtemplate-100x1k Mutates one subtemplate prop's source across 25 cards each invoking 4 inner snippets, 1000 cycles. Snippet bodies shoul…
template:stable-ref-mutate-500 Replaces one item by index in a 500-item list across 100 cycles. Only that item's expressions re-render.
template:subtemplate-data-blob-100 Mutates one field inside data=expression on 100 children. Every child re-renders by design.
template:subtemplate-helpers-heavy-100x500 100 subtemplates, 4 inner bindings where three call helpers shaped like userland reality — Intl.NumberFormat, Array.fin…
template:subtemplate-helpers-light-100x500 100 subtemplates, 4 inner bindings each calling formatDate / classIf / capitalize, 500 cycles. Mutates one source signa…
template:subtemplate-reactive-data-100x500 Mutates one verbose reactiveData field across 100 child subtemplates, 500 cycles. Only the changed field re-evaluates.
template:subtemplate-shorthand-props-100x500 Mutates one shorthand prop's source across 100 child subtemplates, 500 cycles. Only that prop re-evaluates.
todo:add-20 Appends 20 todo items one at a time, like a user typing entries in a row.
todo:bulk-add-500 Renders 500 todo items added at once from a single data load.
todo:clear-completed-250 Clears 250 completed items from a 500-item list in one action, like clicking clear completed.
todo:edit-cycle-5 Runs 5 full edit-then-save cycles on different items, like editing a row and saving it.
todo:edit-start-10 Enters edit mode on 10 different items in a row, like double-clicking each one.
todo:filter-cycle-20 Cycles through active, completed, and all filters 20 times on a 100-item list.
todo:remove-50-back Deletes 50 items from the end of a 100-item list, one click at a time.
todo:remove-50-front Deletes 50 items from the front of a 100-item list, one click at a time.
todo:remove-50-middle Deletes 50 items from the middle of a 100-item list, one click at a time.
todo:remove-first-100 Deletes the first item 100 times from a 200-item list, with remaining items moving up each time.
todo:remove-last-100 Deletes the last item 100 times from a 200-item list, with no other items needing to move.
todo:remove-middle-100 Deletes the middle item 100 times from a 200-item list, walking halfway through to find each target.
todo:rename-500 Renames items in a 100-item list 500 times via single-field setProperty without editingId co-fires.
todo:toggle-100 Cycles through the first 10 items 10 times each, like a user toggling items repeatedly down a list.
todo:toggle-all-200 Toggles all 100 items completed and back across 200 cycles via the master checkbox.
todo:toggle-first-100 Toggles the first item in a 100-item list 100 times, alternating completed on and off.
todo:toggle-last-100 Toggles the last item in a 100-item list 100 times, alternating completed on and off.
todo:toggle-middle-100 Toggles a middle item in a 100-item list 100 times, alternating completed on and off.

Sample size: 80 floor / 270 max · Noise floor: ±2% · Timeout: 3min · Wall-clock: 12m45s

@github-actions github-actions Bot added the Docs Modifies documentation label Jul 2, 2026
@jlukic jlukic changed the title Feat(reactivity): Positional writes wake keyed readers of the same element Feat(reactivity): ReactiveObject Improve Keyed Path Perf Jul 2, 2026
@jlukic jlukic merged commit b535a29 into main Jul 2, 2026
23 checks passed
@jlukic jlukic deleted the feat/keyed-write-canonicalization branch July 2, 2026 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Docs Modifies documentation Reactivity Modifies reactivity package Tests Modifies tests Utils Modifies utilities package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant