When an React
application logic becomes complex, the time spent on component rendering will increase significantly. If the time from component rendering to view rendering takes too long, users will perceive the page to be stuck.
To solve this problem, there are two methods:
- Let the component render process change from synchronous to asynchronous so that
render
the process page will not get stuck. This is how concurrent updates work - Reduce
render
the number of components required, which is often referred to asReact
performance optimization
Usually, we will take the above different methods for different types of components. For example, for the following input box with time-consuming logic, method 1 is more suitable (because concurrent updates can reduce lags during input):
function ExpensiveInput ( {onChange, value} ) { // Time-consuming operations const cur = performance. now (); while (performance. now () - cur < 20 ) {} return < input onChange = {onChange} value = {value}/ > ; }
So, can these two methods be taken into account at the entire application level? The answer is – not really.
This is because, for complex applications, concurrent updates are usually at odds with performance optimization. That’s what this article is going to talk about – the concurrency paradox.
Welcome to join the human high-quality front-end exchange group and lead flying
Contents
Let’s start with performance optimization
For a component, if you want it to remain unchanged when necessary render
, the basic condition that needs to be met is: props
the reference remains unchanged.
Child
For example, the component dependency in the following code fn props
is fn
inline form, so the reference will change every time App
the component render
is used, which is not conducive to Child
performance optimization:
function App () { return < Child fn = {() => {/* xxx */}}/> }
For Child
performance optimization, it can be fn
extracted:
const fn = () => { /* xxx */ } function App () { return < Child fn = {fn}/ > }
When fn
relying on something props
or state
, we need to use useCallback
:
function App ( {a} ) { const fn = useCallback ( () => a + 1 , [a]); return < Child fn = {fn}/ > }
Similarly, other types of variables need to be used useMemo
.
That is to say, when it comes to performance optimization, React
the code logic will become complicated (reference changes need to be considered).
When the application becomes more complex, it will face more problems, such as:
- complex
useEffect
logic - How status is shared
These problems will overlap with performance optimization issues, eventually leading to applications that are not only complex in logic but also have poor performance.
Solutions to performance optimization
Fortunately, these problems have a common solution-state management.
We talked above that for performance optimization, the key issue is – keeping props
the reference unchanged.
In native React
, if a
depends b
, b
depends c
. Then, a
after changes, we need to maintain the stability of references through various methods (such as useCallback
, useMemo
) .b
c
Doing this itself (keeping the reference unchanged) is an additional mental load for the developer. So, how does state management solve this problem?
The answer is: the state management library manages all original and derived states by itself.
for example:
- In
Recoil
, the base state type is calledAtom
, and other derived states are based onAtom
combinations - In
Zustand
, the basic states arecreate
instances created by methods - In
Redux
, a global state is maintained, and the states that need to be usedselector
are extracted from it.
These state management solutions will maintain all basic states and derived states themselves. When developers import state from a state management library, they can keep props
references unchanged as much as possible.
For example, the following example uses Zustand
the above code to modify it. Since state a
and dependencies a
are fn
managed Zustand
by fn
:
const useStore = create ( set => ({ a : 0 , fn : () => set ( state => ({ a : state. a + 1 })), })) function App () { const fn = useStore ( state => state. fn ) return < Child fn = {fn}/ > }
Problem with concurrent updates
Now we know that the general solution for performance optimization is to maintain a set of logically self-consistent external states through a state management library ( the external state here is React
a state that is different from itself), and keep the references unchanged.
However, this set of external states will eventually be converted into React
internal states (and then the changes in the internal states drive view updates), so there is a problem of state synchronization timing. That is: when to synchronize external state with internal state?
Before concurrent updates React
, this wasn’t a problem. Because updates are synchronized and will not be interrupted. Therefore, the same external state can remain unchanged throughout the update process.
For example, in the following code, since List
the component render
process will not be interrupted, it list
is stable during the traversal process:
function List () { const list = useStore ( state => state. list ) return ( < ul > {list.map(item => < Item key = {item.id} data = {item}/ > } </ ul > ) }
However, for concurrent updates enabled React
, the update process may be interrupted, and different Item
components may be in different macro tasks before and after the interruption render
, and the information passed to them data props
may not be the same. This leads to the same update and the same state (in the example list
) being inconsistent.
This situation is called tearing
(view tearing).
It can be found that the cause is – there is a problem with the synchronization timing between tearing
the external state (the state maintained by the state management library) and the internal state.React
This problem React
is difficult to solve currently. The next best thing, in order to allow these state libraries to be used normally, React
a special one was created—— hook
. useSyncExternalStore
It is used to execute updates triggered by the state management library in a synchronous manner, so that there will be no synchronization timing issues.
Since it is executed in a synchronous manner, it must not be updated concurrently~~~
Summarize
In fact, any library that maintains an external state (such as an animation library) involves state synchronization issues and is likely to be incompatible with concurrent updates.
So, which of the following options would you prefer:
- No
care
concurrent updates.React
How you used it before can be used now. - According to the project situation, balance the demands of concurrent updates and performance optimization