Results, 2026-10-08
| Engine | Hits | LCP ms | TBT ms | INP ms | DOM nodes | Images on load (KB) | Frames > 33 ms in scroll | Long frames / blocking ms in scroll | Tiles in DOM after scroll |
|---|
| TanStack | 100 | 772 | 36 | 40 | 300 | 40 (1 165) | 0 | 0 / 0 | 100 (all) |
| TanStack | 2 000 | 580 | 35 | 32 | 257 | 30 (878) | 1 | 0 / 0 | 53 |
| React Aria | 100 | 668 | 361 | 328 | 1 935 | 39 (1 137) | 1 | 0 / 0 | 100 (all) |
| React Aria | 2 000 | 544 | 88 | 104 | 634 | 26 (759) | 21 | 13 / 40 | 32 |
CLS was 0 in every run. Measured after fixing a crash that made pages over 200 hits skip server rendering. The rows themselves still appear only after hydration, once the width is measured. INP is the slowest interaction: switching the layout to the grid and back (both engines), and arrow keys (React Aria). Scrolling is 40 wheel steps of 400 px.
What it shows
- TanStack is smooth. No long animation frames while scrolling, every interaction under 50 ms, about 250–300 DOM nodes at any result size.
- React Aria costs more per tile. Each tile is a gridlist row with a checkbox and two buttons, wrapped by the Virtualizer: about 19 DOM nodes per tile against 3. Without the Virtualizer (100 hits) switching layout takes about 330 ms, which is "needs improvement" for INP (over 200 ms). Virtualized (2 000 hits) it stays responsive but drops frames while scrolling.
- Not like for like. The React Aria tiles carry im-web's interactive overlay; the TanStack tiles are a link with the image and a focus ring. Adding the same overlay to TanStack tiles would raise its numbers too. The comparison to make before choosing is TanStack with an equivalent overlay and hand-built keyboard support.
- Images are no longer the bottleneck. 256 px previews average about 29 KB; the first screen is about 1 MB instead of about 4 MB with 512 px previews. LCP varies with the network (the preview server is on the internet), so compare it over several runs.
React Aria: what it gives, what it lacks
| React Aria GridList + Virtualizer | TanStack Virtual |
|---|
| Screen-reader semantics | Grid with row count and index | None (to build) |
| Keyboard | Left/right, Home/End, PageUp/PageDown, typeahead. Up/down doesn't work in justified rows: it only moves to a tile at exactly the same x, and a layout can only override left and right. We added a spatial up/down handler (keyNear). | None (to build) |
| Selection | Multiple, shift-range, checkbox slot, select-all, onAction separate from select | None (to build) |
| Focus while scrolled away | The focused tile stays rendered | Lost when its row leaves the DOM |
| Load more | Sentinel item; asked twice for the same page in testing, so the page dedupes | Our two triggers, deduped |
| Window scroll | Yes (undocumented, in the source) | Yes |
| Layout | Custom Layout subclass, semi-documented API: pin the version | Our own rows |
| Small results without virtualization | GridList in a flex-wrap, sized by the same packing | Rows in normal flow with content-visibility |
| Cost | react-aria-components 1.21.1; more DOM and script per tile (above) | @tanstack/react-virtual, small |
Open
- Decided 2026-10-08: TanStack (decision IG13). The React Aria version stays as the comparison, without further tuning.
- DevTools trace, 2026-10-08 (React Aria, 2 000 hits, scrolling 18 000 px, CPU 4×): Chrome flags no forced reflows, so React Aria's long frames are rendering work, not its scroll handler measuring layout. Its image insight: the preview server sends JPEG only; WebP or AVIF would save 10–25 KB per image. That is for the image service, not the grid.
- When the TanStack tiles get im-web's overlay (checkbox, Download, Add) and keyboard support, rerun the script to see what they cost.
- Server-render the first rows, so LCP doesn't wait for hydration and the width measurement.