Repository navigation
Conversation
…nimatedLayout in a long list Cindy asked for a perf test of useAnimatedLayout for an item in the middle of a long list. The page follows Markdown Perf's layout: heading and description, a muted toolbar, a Run metrics card beside the output. A list of 1k, 5k or 10k rows (every row rendered, or a virtualized window) has a Collapsible in the middle that takes the list's handle; a run expands and collapses it six times and reports the commit and walk until the glide starts, rectangles read, elements animated, time to the first frame, long tasks, frames and drops, each against a saved baseline. Sandbox only: no package change.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
AI review status for this pull request.
|
PR Analysis Report
|
… run measures its commit and the walk, not a list re-render The open state lived on the page, so each toggle re-rendered every row (10k rows: most of the 275 ms). The middle item now owns it, and the rows are built once per list and window.
…, the browser's own layout cost A Glide control (With handle / No handle) runs the same list without useAnimatedLayout, so the walk's share of a toggle is the difference.
There was a problem hiding this comment.
Changes requested: the page runs as described in Chromium, and its numbers show the walk growing with the rows after the change, but its comparison flow can mislead whoever reads the numbers. Please fix these before it lands with its base:
- Saved baselines live only in component state, so the reload a build change needs clears them, although the help text and description promise cross-build comparisons (
page.tsx:190,:475-477). - Stop publishes a partial run under the fixed "6 toggles" badge and lets it be saved as the baseline (
page.tsx:429,:444). - With six toggles,
median()returns the upper middle value of three opens and three closes, so it reports only one kind of toggle (page.tsx:120-125). - The middle item's copy says every row after it glides and that the walk visits the rows around it; only the rows in view move, and the walk reads the elements after the item (
page.tsx:168-172).
[Automated review]
| frame is a measured animation frame gap over 20 ms. Save a | ||
| baseline, change the build or the CPU throttle in DevTools, and | ||
| run again to compare. |
There was a problem hiding this comment.
Baselines live only in useState (line 190), so the reload a build change needs clears them, and every metric then reads "no baseline". Persist them in sessionStorage or localStorage keyed by rows:rendering:glide, or limit this sentence to in-session changes such as the CPU throttle.
| label="Save as baseline" | ||
| variant="secondary" | ||
| onClick={saveBaseline} | ||
| isDisabled={isRunning || metrics.walkMs == null} |
There was a problem hiding this comment.
After Stop, the partial run's metrics are published under the fixed "6 toggles" badge (line 444), and this button stays enabled because walkMs is set. A run stopped after three toggles showed Frames 168 and Long tasks 3, against 335 and 6 for a full run. Leave the metrics empty when a run is stopped, or record the completed toggle count, show it in the badge, and enable Save only for a complete run.
| return null; | ||
| } | ||
| const sorted = [...values].sort((a, b) => a - b); | ||
| return sorted[Math.floor(sorted.length / 2)] ?? null; |
There was a problem hiding this comment.
With six toggles this returns sorted[3], the upper of the two middle values. A run alternates three opens and three closes, so this reports the cheapest toggle of whichever kind costs more (Rectangles read: 5,007 for a full 5,000-row run, 5,001 for a three-toggle run). Average the two middle values, or report opens and closes separately.
| Expanding this item glides every row after it in the list. | ||
| </Text> | ||
| <Text type="supporting"> | ||
| The walk visits the rows around it, at each level up to the list, | ||
| and moves only those whose rectangle changed and that are in view. |
There was a problem hiding this comment.
Only the rows in view glide (9 of the roughly 2,499 after this item), and the walk reads the elements after the item, not around it. Suggested copy: "Expanding this item moves the rows after it that are in view. The walk reads every element after it, at each level up to the list, before and after the change."
…ports nothing for a stopped run, and takes a true median - Saved baselines go to localStorage per rows, rendering and glide, so the reload a build change needs keeps them (read after hydration). - A stopped run publishes no metrics, so a partial run can neither read as a full one nor be saved as a baseline. - The median of an even count is the mean of its two middle values: a run's three opens and three closes both count. - The middle item's copy says what moves (the rows after it that are in view) and what the page measures.
There was a problem hiding this comment.
Changes requested: the page builds, its route exports, and the earlier findings are fixed, but two states still mislead whoever reads its numbers. Please fix these before it lands with its base:
- No handle runs stop the clock before the browser's style and layout, so the help text's "with handle minus no handle" charges the walk about 5 ms at 5,000 rows and 9 ms at 10,000 that the browser spends anyway; add a layout read such as
void viewport.offsetHeight;beforewalks.push(page.tsx:327), or say that No handle times React's commit only. - After a reload every metric reads "no baseline" although the baseline is saved; show "no baseline" only when none is saved for the configuration (
page.tsx:109-112).
[Automated review]
| // The layout walk runs in a microtask after the commit. | ||
| await Promise.resolve(); | ||
| await Promise.resolve(); | ||
| walks.push(performance.now() - start); |
There was a problem hiding this comment.
With No handle nothing between start and this line reads layout, so the browser's style and layout for the change run after the clock stops (4.3–5.5 ms at 5,000 rows and 8.3–10.6 ms at 10,000 in Chromium), while with the handle the walk forces that same layout inside the window. A layout read the counter doesn't see, such as void viewport.offsetHeight; just before this line, puts it in both modes, so with-minus-without is the walk alone; otherwise the help text at line 514 should say No handle times React's commit only.
|
|
||
| /** A value's change from its baseline, as the other perf pages show it. */ | ||
| function formatChange(value: number | null, base: number | null): string { | ||
| if (value == null || base == null) { |
There was a problem hiding this comment.
With no finished run value is null, so every metric reads "no baseline" even when a baseline for this configuration is saved, which is what a maintainer sees right after the reload the help text says baselines survive. Return "no baseline" only when base == null, and show the saved value (or "baseline saved") while value is null.
…th modes, and a saved baseline reads as saved after a reload - An uncounted layout read closes each toggle's clock, so No handle times the browser's own style and layout for the change too, and with the handle minus without is the walk alone. - A metric reads no baseline only when none is saved for the configuration; before a run it reads baseline saved.
There was a problem hiding this comment.
Approval recommended: the page builds, its route exports, and the two issues from the last review are fixed, so No handle now includes the browser's layout and a saved baseline reads as saved after a reload. Before #8634 compares against it, consider refreshing the description's table, which predates the last two commits (it shows 5,007 and 10,007 rectangles, and No handle times taken without the browser's layout). The page can land with or after its base, since useAnimatedLayout and Collapsible's animatedLayout prop are not on main yet.
[Automated review]
| // Virtualized: only the rows in (and just around) the viewport render; the | ||
| // middle item always renders, so the change always happens in the DOM. |
There was a problem hiding this comment.
Optional: this says the middle item always renders, but the loop at lines 392-407 renders it only inside the window, so in Virtualized mode it is absent from the DOM until a run scrolls to it. Every run scrolls there before its first toggle, so the numbers are unaffected; rewording the comment would match the code.
Stacked on #8568, which makes
useAnimatedLayoutand Collapsible'sanimatedLayoutprop public. The sandbox uses public APIs only, so this page waits for #8568 and #8482. Retarget tomainonce they land.Primary intent
A fullscreen sandbox page that measures
useAnimatedLayoutfor an item in the middle of a long list. It sits beside Markdown Perf and DiffViewer Perf and follows their layout. Sandbox only: no package change and no Changeset. It's "other" because it's a measurement tool, not a product change.User or maintainer impact
Maintainers can see what a glide costs at scale before the module's walk changes:
Problem and solution fit
The module walks the content after a change and reads its rectangles. Nothing so far showed what that costs in a long list, or under a slower CPU. This page does, with the controls the question needs (row count, rendering), and baselines for before/after comparisons across builds.
Authority
The request for a
useAnimatedLayoutperf test for items in the middle of a long list.spec:AST-105andarchitecture:react-component-runtimeINV12 define what the module does; this page only measures it.Change and evidence
Real Chromium, the built sandbox page, median over 6 toggles of the middle item. "No handle" is the same list without
useAnimatedLayout, so it shows the browser's own cost (each toggle's clock closes with a layout read in both modes, so the difference is the walk):The walk is linear in the rows after the change, and the page now shows that. #8634 bounds it and reports the after numbers from this page.
Desktop, after a run (5k rows, every row), with the middle item open:
390 px. The list sits below the metrics; nothing in view moves, so 0 elements animate:
Reduce Motion, desktop. With a fine pointer the change lands at once, so 0 elements animate:
Scope
Testing
pnpm -F @astryxdesign/sandbox build: prerender, plus export verification against the route list, which gains/pages/animated-layout-perf/.