Performance of useSyncExternalStore and React transitions
What version of React, ReactDOM/React Native, Redux, and React Redux are you using?
- React: 18.2.0 (latest at time of writing)
- Redux: 4.2.1 (latest at time of writing)
- React Redux: 8.1.3 (latest at time of writing)
What is the current behavior?
Reduced test case: https://github.com/OliverJAsh/react-redux-useSyncExternalStore-perf
Despite wrapping calls to dispatch with startTransition, React will always render Redux state updates synchronously. This is in contrast to regular React state updates.
I suspect this is due to the behaviour of useSyncExternalStore. However I was wondering if you're aware of any plans for this to be addressed, either in React Redux somehow or React itself?
As it stands I think this is a significant performance issue. For example, here's a performance recording from Unsplash (the application I work on). We're able to benefit from concurrent rendering and "time slicing" for hydration, but then once hydration has finished we need to run some state updates through Redux, and this results in this very long task (hovered):
It would be great if we could benefit from transitions / concurrent rendering / time slicing.
In our case at Unsplash, if we could figure out a way to make these states updates non-blocking then we could significantly improve our page load performance and metrics such as TBT, TTI, and ITP.
If it helps, here's a demo of my reduced test case, comparing state updates for React vs Redux (both wrapped in startTransition). Each state update renders 10 components, each taking 100ms.
https://github.com/reduxjs/react-redux/assets/921609/27d767e3-dfb1-469c-9c2c-f64a8b1e9038
What is the expected behavior?
N/A
Which browser and OS are affected by this issue?
No response
Did this work in previous versions of React Redux?
- [ ] Yes
It is pretty much the React team's decision to deal with all external data sources like this - they created useSyncExternalStore for exactly the purpose of keeping "non-React" updates synchronous (to avoid tearing if a non-React datasource updates during a render), and there is no other way (or only less performant ways) we could implement this without useSyncExternalStore.
Of course, there is also no efficient way of implementing anything like this with only React means - Context is not a solution here :/
That's what I thought. Do you know if there's been any further discussion in the React team about this? I understand the constraints but this seems like a pretty fundamental flaw to me.
Purely off the top of my head, this is basically the React team's intent. The only way React can fully know that a state update is interruptible / low-pri is if it's built-in React state, in which case React controls when the render pass happens and when that queued state update gets applied. React has no control over any external store, and those update synchronously, so there's no way to tie together a future pending React update with the external store somehow applying the change to itself later when React is ready to render. So, React is forced to treat all external state updates and renders as synchronous and blocking too.
I'm suffering from the same issue too. This sync work blocks rendering for so much ms. Are there at least any recommended workaround? 👀
What I'm trying is using useDeferredValue to defer from original state whenever I need transitions.
In our case we were able to workaround the problem by moving the state out of Redux and into context. This does make me question the long term usage of Redux state in React. 🤔