Context
Found while building the acceptance journey of epic #19658 (#19666), on its 100k-row example, which is not on dev yet. At 1400×900, jumping to row 99996 and then pressing ArrowDown four times in a row leaves the final row (record 100000) 100px tall, while its tallest text needs 81px. 100px is the height of record 99968, the previous occupant of the same pooled row slot. Without the fix, 18 of 20 headless runs ended with the stale 100px. With the fix, 0 of 20 did.
The Problem
grid.View#measureRowHeights collects each target from a row's vdom: the record, the cell, the text container's id and the width. It reads the containers' rects from the DOM and attributes each rect to the record the vdom named.
The DOM can lag the vdom while nothing on the row's path is updating. syncBodies recycles the rows silently (createViewData(true)), then calls the view's update(). While other rows have updates in flight, here the rows that keyboard selection repaints, the view's update waits for them (VdomLifecycle#updateVdom, the isChildUpdating branch), and that wait sets no flag. So the view, its bodies and the recycled row all report isVdomUpdating and needsVdomUpdate as false. measureRowHeights then reads a container that still shows the previous record's text.
The reply-time fence (row.record, row.vdom.cn.includes(cell), the width) compares the vdom with itself, so the read passes. #measuredCells then marks the cell as measured, so the stale height stays until the cell renders again.
Instrumented runs (temporary logging, not committed) showed:
- An 8-run batch: in both failing runs, the four wrapText targets of record 100000 were collected while their vnode text differed from their vdom text. In none of the six passing runs did that happen.
- A 10-run batch that also logged the update flags: two runs collected such targets. Each time, the view, the body, the row and the grid container were idle. The only updates in flight were those of the two neighbouring row slots in each body.
The Architectural Reality
src/grid/View.mjs, measureRowHeights: target collection and the reply-time fence.
src/grid/View.mjs, syncBodies: silent recycling, then one view update.
src/mixin/VdomLifecycle.mjs, updateVdom: an update that waits for a parent's or a child's flight sets no flag.
- A component's
vnode is the App worker's copy of what its last landed update rendered.
- Every landed view, body and row update schedules a measuring pass (
afterExecuteVdomUpdate → scheduleRowMeasure).
- Design authority: #19662's contract that every row is as tall as its tallest wrapped text plus the cell chrome, as stated in
measureRowHeights' JSDoc.
The Fix
A cell is only collected if the DOM shows it as its vdom describes it. Its vnode (VNodeUtil.getById(row.vnode, cell.id)) must hold the same text container, the same text and the same width. A skipped cell is measured by the pass that its row's landed update schedules.
Acceptance Criteria
Out of Scope
- Keyboard navigation keeping its target in view after measurement (a separate defect from the same journey).
- The hidden pre-render (#19663).
Avoided Traps
- A flag check (
needsVdomUpdate): the waiting view update sets none.
- Skipping the pass while any row update is in flight: too coarse. It would starve measurement during keyboard navigation.
- A reply-time text check against the vdom: the vdom already names the new record, so it cannot see a lagging DOM.
Related
#19658 (epic) · #19662 (in-place measurement) · #19666 (the journey that found it)
Live latest-open sweep: checked the latest 20 open issues at 2026-10-12T22:30:34Z, and the latest 8 again right before filing; no equivalent found.
A2A claim sweep: the last 30 messages; no competing claim. #19663's author leaves the jump and final-row boundary to #19666.
MC sweep: the query "grid content-sized row takes previous record height recycled row slot stale measurement measureRowHeights vdom ahead of DOM" returned 6 results; no prior decision found.
Own-assignment sweep: 8 open; #19666 (the journey) and #19658 (the epic) are the parents, none overlapping.
Origin Session ID: daac81df-5205-405e-a3af-aec56e5cca45
Context
Found while building the acceptance journey of epic #19658 (#19666), on its 100k-row example, which is not on
devyet. At 1400×900, jumping to row 99996 and then pressing ArrowDown four times in a row leaves the final row (record 100000) 100px tall, while its tallest text needs 81px. 100px is the height of record 99968, the previous occupant of the same pooled row slot. Without the fix, 18 of 20 headless runs ended with the stale 100px. With the fix, 0 of 20 did.The Problem
grid.View#measureRowHeightscollects each target from a row's vdom: the record, the cell, the text container's id and the width. It reads the containers' rects from the DOM and attributes each rect to the record the vdom named.The DOM can lag the vdom while nothing on the row's path is updating.
syncBodiesrecycles the rows silently (createViewData(true)), then calls the view'supdate(). While other rows have updates in flight, here the rows that keyboard selection repaints, the view's update waits for them (VdomLifecycle#updateVdom, theisChildUpdatingbranch), and that wait sets no flag. So the view, its bodies and the recycled row all reportisVdomUpdatingandneedsVdomUpdateas false.measureRowHeightsthen reads a container that still shows the previous record's text.The reply-time fence (
row.record,row.vdom.cn.includes(cell), the width) compares the vdom with itself, so the read passes.#measuredCellsthen marks the cell as measured, so the stale height stays until the cell renders again.Instrumented runs (temporary logging, not committed) showed:
The Architectural Reality
src/grid/View.mjs,measureRowHeights: target collection and the reply-time fence.src/grid/View.mjs,syncBodies: silent recycling, then one view update.src/mixin/VdomLifecycle.mjs,updateVdom: an update that waits for a parent's or a child's flight sets no flag.vnodeis the App worker's copy of what its last landed update rendered.afterExecuteVdomUpdate→scheduleRowMeasure).measureRowHeights' JSDoc.The Fix
A cell is only collected if the DOM shows it as its vdom describes it. Its vnode (
VNodeUtil.getById(row.vnode, cell.id)) must hold the same text container, the same text and the same width. A skipped cell is measured by the pass that its row's landed update schedules.Acceptance Criteria
9ee0733a44)WrapTextMeasure(unit and e2e) andAnchorCorrection(unit and e2e) stay green. (CI e2e green at9ee0733a44; the grid unit specs skip in CI, and the local full unit suite passed 5045, plus 2 skipped)Out of Scope
Avoided Traps
needsVdomUpdate): the waiting view update sets none.Related
#19658 (epic) · #19662 (in-place measurement) · #19666 (the journey that found it)
Live latest-open sweep: checked the latest 20 open issues at 2026-10-12T22:30:34Z, and the latest 8 again right before filing; no equivalent found. A2A claim sweep: the last 30 messages; no competing claim. #19663's author leaves the jump and final-row boundary to #19666. MC sweep: the query "grid content-sized row takes previous record height recycled row slot stale measurement measureRowHeights vdom ahead of DOM" returned 6 results; no prior decision found. Own-assignment sweep: 8 open; #19666 (the journey) and #19658 (the epic) are the parents, none overlapping.
Origin Session ID: daac81df-5205-405e-a3af-aec56e5cca45