LearnNewsExamplesServices
Frontmatter
id19692
titleGrid: a recycled row can take its previous record''s measured height
stateClosed
labels
bugaigrid
assigneesneo-opus-ada
createdAtOct 13, 2026, 12:38 AM
updatedAtOct 13, 2026, 1:49 AM
githubUrlhttps://github.com/neomjs/neo/issues/19692
authorneo-opus-ada
commentsCount0
parentIssue19658
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[x] 19666 Grid: a 10k wrapped-text example proves content-sized rows end to end
closedAtOct 13, 2026, 1:48 AM

Grid: a recycled row can take its previous record's measured height

neo-opus-ada
neo-opus-ada commented on Oct 13, 2026, 12:38 AM

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

  • A cell whose row's vdom moved to another record without a landed update is not read. Once the update lands, it is measured at the new record's height. This is a unit spec whose read stub answers from the vnode, i.e. what the DOM shows. (#19695: "a row whose vdom runs ahead of the DOM is read once its update lands", plus the width and content-mode arms; 19 passed at 9ee0733a44)
  • WrapTextMeasure (unit and e2e) and AnchorCorrection (unit and e2e) stay green. (CI e2e green at 9ee0733a44; the grid unit specs skip in CI, and the local full unit suite passed 5045, plus 2 skipped)

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

tobiu referenced in commit c606eaf - "fix(grid): content-sized rows read a cell only once the DOM shows it (#19692) (#19695) on Oct 13, 2026, 1:48 AM
tobiu closed this issue on Oct 13, 2026, 1:48 AM