Skip to content

perf(List): skip re-rendering unchanged lists and tables - #3575

Merged
mfal merged 3 commits into
nextfrom
perf/list-skip-unchanged-renders
Oct 27, 2026
Merged

mfal merged 3 commits into
nextfrom
perf/list-skip-unchanged-renders

Conversation

@mfal

@mfal mfal commented Oct 26, 2026

Copy link
Copy Markdown
Member

A refetch that returns the same items, or a re-render of a list's parent, no longer re-renders the list or table. Two changes, one commit each:

  1. ListLoaderState.setDataBatch keeps a batch whose items did not change (components-base, shared by the React and Vue bindings). A loader that builds a new array on every fetch, typically a refetch whose data layer reuses the item instances, replaced the batch anyway. The merged data changed, and the whole list re-rendered. Now the batch stays when the new one holds the same items in the same order.

  2. List.Table skips rendering while nothing it shows has changed. react-aria re-renders every row and cell whenever its table renders, because TableStateContext gets a new value on each render (Table: TableStateContext changes identity every render, so ResizableTableContainer re-renders every cell on width changes adobe/react-spectrum#10759). Flow's row memo only covers react-aria's hidden collection pass. So every render of the list's parent re-ran useTableRow and useTableCell for all rows. The table now returns its last element while these stay equal:

    • loading state, onAction, the infinite scroll trigger
    • every entry's id and data
    • the list, table, header, column, body and row props (new rendersSameAs on the table models, alongside the existing TableRow.rendersSameAs)

    Like the row memo, this needs renderDependencies on the row. The setup elements' children no longer go into the table models' props, since the models already hold them as header, body, columns and row.

adobe/react-spectrum#10816 addresses the same row re-renders inside react-aria. It would also help when a row does change, which this PR does not cover.

Related: #3528

Effect

Measured in mStudio on an invoice table with 160 rows, 6 columns and renderDependencies. Dev build, React Profiler render time summed over all commits of one refetch, median of 10 runs:

Refetch before after
only the list's queries, items unchanged 677–710 ms 1.5 ms
every query on the page, items unchanged 876–1012 ms 138–142 ms

Without the patch a list-only refetch took 5 commits, each re-rendering all 160 rows' and 960 cells' react-aria wrappers. With it, only the loader component renders.

Tests

  • ListLoaderState.test.ts: a batch with the same items does not reach an observer again; one with a changed item does.
  • List.browser.test.tsx (Table rows): counts react-aria's row renders through a TableRow view whose className function runs on each of them. Two parent re-renders render no row; a change to renderDependencies still does.

Both tests fail without their change (verified by reverting it: 4 row renders instead of 0, 2 observer calls instead of 1). The existing table tests for changed row and cell props cover the cases where the table must render again.

🤖 Generated with Claude Code

mfal and others added 2 commits October 26, 2026 08:45
A refetch hands the loader a new array even when the data layer reuses
every item. setDataBatch replaced the batch anyway, so the merged data
changed and the whole list re-rendered. It now keeps the batch when the
new one holds the same items in the same order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
react-aria re-renders every row and cell of a table whenever the table
renders, because TableStateContext gets a new value on each render
(adobe/react-spectrum#10759). The row memo only covers react-aria's
hidden collection pass, so every render of the list's parent re-ran
useTableRow and useTableCell for all rows.

The table now returns its last element while nothing it renders has
changed: loading state, onAction, the infinite scroll trigger, every
entry's id and data, and the list, table, header, column, body and row
props. Like the row memo, this needs renderDependencies on the row.
The setup elements' children no longer go into the models' props; the
models already hold them as header, body, columns and row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mfal
mfal requested a review from a team October 26, 2026 08:21
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Preview Deployment

Preview environments are ready:

Type URL
docs pr-3575.docs.review.flow-components.de
storybook pr-3575.storybook.review.flow-components.de

Images:

  • docs: ghcr.io/mittwald/flow/docs:pr-3575
  • storybook: ghcr.io/mittwald/flow/storybook:pr-3575

@mfal
mfal enabled auto-merge October 26, 2026 08:37
@mfal
mfal merged commit 212bb8b into next Oct 27, 2026
24 checks passed
@mfal
mfal deleted the perf/list-skip-unchanged-renders branch October 27, 2026 09:39
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.

2 participants