Skip to content

perf(listbox): batch sticky row width reads and writes - #585

Draft
kianbazza wants to merge 1 commit into
ui-642-stop-aim-guard-updates-re-rendering-every-rowfrom
ui-643-batch-sticky-row-width-reads-and-writes
Draft

kianbazza wants to merge 1 commit into
ui-642-stop-aim-guard-updates-re-rendering-every-rowfrom
ui-643-batch-sticky-row-width-reads-and-writes

Conversation

@kianbazza

@kianbazza kianbazza commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Row width measurement wrote the measuring styles to a row and then read its width, one row at a time. Each read after a write forces a synchronous layout, so opening a menu cost one layout per row. Each batch now writes, reads and restores in three passes.

 on animation frame (queued rows)
-  for each row
-    apply measuring styles
-    read scrollWidth / offsetWidth        // forced layout
-    getComputedStyle(row)                 // even with debug off
-    restore styles
+  for each row: apply measuring styles
+  for each row: read scrollWidth / offsetWidth   // one layout for the batch
+  for each row: restore styles
+  track the max width as before
   set --row-width if it grew

Debug logging in the hook now takes its details as a function, so with the debug flag off none of it is computed. That includes the per-row getComputedStyle call, the zero-width diagnostics and the payload objects. Per-row debug output inside a batch is logged once per batch.

Evidence

use-sticky-row-width.test.tsx › "applies measuring styles to every row before reading any width": five items with known widths. It logs, in order, each width read and each measuring write to a row (style.width, style.maxWidth, data-measuring). One row starts with its own inline width and max-width.

forced layouts (write followed by read)    before: 5       after: 1
--row-width                                before: 341px   after: 341px
row with inline width / max-width          150px / 180px restored exactly
  • Before: AssertionError: expected 5 to be 1
    After: passes. Every row ends with its original inline sizes and no data-measuring.

The test also fails for a version that batches the attribute but still writes width and max-width per row between reads (5 forced layouts), and for one that clears inline sizes instead of restoring them.

The existing subpage --row-width test passes unchanged.

Merge Danger

Door: two-way

Blast Radius: small

Every row in a batch is in measuring mode at the same time. Each one has an explicit width: max-content, so its width doesn't depend on its siblings, but unusual custom CSS on [data-measuring] could see a different --row-width.

Closes UI-643

@vercel

vercel Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ui-canary Ready Ready Preview Oct 10, 2026 11:33pm UTC

Request Review

@linear-code

linear-code Bot commented Oct 10, 2026

Copy link
Copy Markdown

UI-643

This branch was successfully deployed

1 active deployment
Preview – ui-canary — 7efc2c8c Deployed Oct 10, 2026 by vercel[bot]
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.

1 participant