Skip to content

fix: Improve performance of collection updates in React transitions #8204

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 8, 2025

Conversation

devongovett
Copy link
Member

@devongovett devongovett commented May 8, 2025

Fixes #8094

This improves performance of collection updates that occur in React transitions, e.g. startTransition. The performance issue was caused by the following behavior described in the docs for useSyncExternalStore:

If the store is mutated during a non-blocking Transition update, React will fall back to performing that update as blocking. Specifically, for every Transition update, React will call getSnapshot a second time just before applying changes to the DOM. If it returns a different value than when it was called originally, React will restart the update from scratch, this time applying it as a blocking update, to ensure that every component on screen is reflecting the same version of the store.

Essentially, React would call our appendChild to update the fake DOM, we'd trigger the useSyncExternalStore subscription, and React would interrupt itself and start over. This would occur for each element that was rendered, resulting in many renders.

This behavior seemed unexpected at first, because in general, React's commit phase is blocking. Only the render phase is concurrent. After inspecting the React source code, it turns out that they actually do some DOM updates during the render phase, which can be interrupted. Specifically, they create new DOM elements during the render phase, and then the commit phase is just attaching new subtrees to the existing DOM, and performing updates/removals.

The solution is to avoid triggering collection updates for elements that are not in the document yet. Once React starts the commit phase, it will attach these disconnected nodes and that will result in a single collection update instead of interrupting render.

@rspbot
Copy link

rspbot commented May 8, 2025

</TableBody>
</Table>
</div>
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

description of what we should see if it's working vs failing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fast vs slow

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I pulled down the PR and reverted the changes in Document. It is definitely painfully slow without the update to Document.
It'd be nice if we could actually test this so we'd know if we ever introduced anything which made it slow again.
I wonder if this can be replicated in a jest test with real timers and if it exceeds some threshold then we fail.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe. I'd be worried about the timing variability on different machines though

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@snowystinger Added this in #8209. Should be safe as far as timing is concerned 👍

</TableBody>
</Table>
</div>
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I pulled down the PR and reverted the changes in Document. It is definitely painfully slow without the update to Document.
It'd be nice if we could actually test this so we'd know if we ever introduced anything which made it slow again.
I wonder if this can be replicated in a jest test with real timers and if it exceeds some threshold then we fail.

@devongovett devongovett added this pull request to the merge queue May 8, 2025
Merged via the queue into main with commit 22a9d27 May 8, 2025
31 checks passed
@devongovett devongovett deleted the test_remix branch May 8, 2025 18:54
nwidynski added a commit to nwidynski/react-spectrum that referenced this pull request May 9, 2025
github-merge-queue bot pushed a commit that referenced this pull request May 13, 2025
…8209)

* feat: add tests for #8204

* chore: review comments

* chore: revert to main branch

* fix: react 16,17 compat
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.

RAC - Table performance very slow when rendering new collections.
6 participants