Repository navigation
Live holes: send and morph only the rows that changed #4064
Description
Follow-up to #4056 (closed after #4057) and #4063.
Problem
A live hole that renders a list re-sends its whole markup whenever any row changes, and the client parses and morphs all of it. #4057 stopped the server from re-rendering unchanged rows during a sweep: a For row whose item is shallow-equal is reused. But the emission is still the whole hole. On the wire, streaming brotli absorbs most of the repetition, so bytes aren't the cost. The client cost is: the parse and morph of every unchanged row, plus layout over the whole range.
The server DBmon example (one live hole holding the whole table; 50% of rows mutate per tick at 5 ticks/s; 4x CPU, Chromium) shows this clearly:
| rows | live view: busy ms/s | ticks applied / 50 | SPA twin: busy ms/s | ticks applied / 50 |
|---|---|---|---|---|
| 100 | ~410 | 50 | ~296 | 50 |
| 400 | saturated | ~31-52 | ~807 | 50 |
| 1000 | saturated | ~13-18 | ~1012 | 46 |
The live view sends 3-7x fewer wire bytes than the SPA twin, but it uses more client CPU and falls behind from 400 rows. Profiles put the morph (parseFragment, reconcileChildren, morphAttributes) and layout ahead of everything else. #4063 (scoping the slot sync to the morphed range) makes no measurable difference here, because the morphed range is the whole table on every tick. The slot walk is about 5-9% of busy time.
Direction
During a sweep, emit row-level ops for a For inside a live hole instead of the whole hole: insert, remove or move a row, or replace one row's markup. The client then applies each op to that row's range only.
Open questions:
- Row identity on the wire. Server For keeps its rows across live-hole sweeps (#4056) #4057 reuses rows by index plus shallow equality, which is enough to skip re-rendering but not to address a row. Keyed moves need a stable key.
$keyis morph-only client identity today; it could double as the row key here. - Row markers. Each row needs a range the client can find. Cached like the hole markers in frames: live writes morph only what changed (scoped slot sync + For row ops) #4063, so applying ops doesn't add a search per op.
- Fallback. When a sweep changes most rows or the row structure, the whole hole should still be re-sent.
- Nested
Forinside a live hole. It latches the hole today (the A live hole silently stops re-emitting when its markup has a conditional on a property ({m.usage && …}) #4023 impurity gate), so a nested list has to be written with.map. Row ops would make a per-row innerFormore valuable, so this overlaps with the A live hole silently stops re-emitting when its markup has a conditional on a property ({m.usage && …}) #4023 decision.
Chat's growing message (one row that only appends) is the degenerate case: an append patch.
Activity
ryansolid commentedon Oct 15, 2026
Generated by Claude Code