Reasonable Component Structure
In React, a reasonable component structure can effectively enhance performance and avoid unnecessary re-renders. Below is an analysis and optimization summary of a simple example.
|
|
When the input value changes, the App component re-renders, causing the Child component to also re-render, which is very time-consuming. We find that Child has no relation to the input value, so we can separate the input into its own component to avoid re-rendering Child.
|
|
Alternatively, we can pass the Child component as children to the parent component, so it won’t re-render when the value updates.
|
|
Through these two optimization methods, we can effectively isolate the rendering logic of the input component and the Child component, thus improving the application’s performance and user experience.
Choosing the right component structure and passing method is an important consideration in React development.
Using API to Reduce Component Rendering
shouldComponentUpdate
shouldComponentUpdate is a method in the React component lifecycle used to control component re-rendering. It is called when the component receives new props or state, returning a boolean value indicating whether the component should update.
|
|
When the rendering performance of a component is crucial, using shouldComponentUpdate can prevent unnecessary rendering.
React.memo
React.memo is a higher-order component provided by React to optimize the performance of functional components. It significantly improves application performance by avoiding unnecessary re-renders, especially in the following cases:
- The component tree is large.
- The cost of rendering the component is high.
Default Behavior
React.memo uses shallow comparison of props by default to determine whether the component should re-render.
|
|
Custom Comparison Function
If the props are complex objects or arrays, you may need to provide a custom comparison function. When using a custom comparison function with React.memo, the return value has the following meanings:
- Return true: Indicates that the previous and current props are the same, and the component will not re-render.
- Return false: Indicates that the previous and current props are different, and the component will re-render.
|
|
useDeferredValue
Refer to the official documentation: useDeferredValue
In certain scenarios, we may need to pass the value of an input box to a child component, causing the child component to re-render every time the parent component updates. This can affect user experience, especially when the child component renders slowly. To solve this problem, we can use the useDeferredValue hook.
Consider the following code:
|
|
In this example, the value of our input box is passed to the Child component via props. Every time the input box content changes, the Child component re-renders, leading to performance issues.
Debouncing or Throttling
To solve this problem, we can use debouncing or throttling techniques. In this case, we can set a childValue state to delay the rendering of the Child component.
|
|
The value of the input box will update first, and then the Child component will update after 200ms. Using debouncing or throttling only partially optimizes the input box experience, as the input box may still feel unresponsive during the rendering of the Child component.
Because during the rendering of the Child component, if input operations continue, the rendering of the Child component occupies the JS engine, and the input box events will not respond. This means that during the next input, the ongoing rendering of the Child component will not be interrupted to execute higher-priority input event tasks.
At this point, we can use useDeferredValue to solve this problem.
Using useDeferredValue
useDeferredValue accepts a deferred value. When the deferred value changes, it first renders using the old value, and then re-renders in the background using the new value. In other words, React will render the component twice: once to display the old result and once to display the new result.
The key point is that the background rendering can be interrupted, meaning that when there are higher-priority state updates, the rendering of the Child component can be interrupted by rendering the input box, resulting in a smoother experience compared to debouncing and throttling.
|
|
useTranstion
Official documentation: useTransition
useTransition is a hook introduced in React 18 to handle the transition state of UI updates. It allows you to mark certain updates as “non-urgent,” so React can prioritize handling user input and other urgent updates while processing these non-urgent updates in the background, improving application responsiveness and user experience.
In this example, we set an intermediate value childValue and update it as a transition update. When the value of the input box updates, the transition update will be interrupted, and after the input box updates, the transition update will execute again.
|
|
Upgrade to React 18 or Above
Starting from React 18, React provides automatic batching and concurrent rendering。
Automatic Batching
In earlier versions of React, updates could only be batched in synthetic events (such as click events). This meant that if you performed multiple state updates in an asynchronous function (like a Promise or setTimeout), React would process these updates one by one, leading to multiple renders.
React 18’s automatic batching allows multiple state updates to be automatically merged anywhere (including in asynchronous functions). This feature reduces the number of renders, thus improving performance. For example:
|
|
Concurrent Rendering
Concurrent rendering is another important feature of React 18, allowing React to handle updates more flexibly, thus enhancing the smoothness of the user interface. Suppose there’s a Child component that takes about 200ms to render, but the rendering time for each frame in the browser is typically about 16ms. During the rendering of the Child component, the page will not respond to user interactions.
Fiber Nodes and Task Management
Each JSX element is converted into a corresponding Fiber node, and each Fiber node has its own task. For example:
Host nodes (like div, span): create corresponding real DOM nodes and set styles and attributes。
Custom components (like Child): manage the children within the component and handle their state and side effects。
When the Child component contains a large number of nodes (for example, 10,000), React processes each Fiber node in a depth-first manner, which can take a lot of time. React 18’s concurrent rendering solves this problem by breaking the rendering process into multiple small tasks.
Specific Implementation
In each frame, React reserves some time (for example, 5ms) to process the first 1,000 (assumed value) nodes of the Child component. As follows:
First Frame:
Process the first 1,000 nodes, taking about 5ms. The remaining time (11ms) is used to respond to user clicks and input events, ensuring the page remains responsive.
Subsequent Frames:
If there are no user interactions, React continues processing the remaining 9,000 nodes in the next frame. If user interactions occur (like clicks or input), React prioritizes handling these high-priority tasks.
Task Resumption:
After handling high-priority tasks, React resumes the rendering tasks of the Child component from the last interrupted point, continuing to process the remaining Fiber tasks.
In this way, React can allocate time across multiple frames, ensuring that the user interface remains smooth and responsive. Ultimately, the entire rendering task of the Child component will be completed in about 10 frames, without affecting the user interaction experience.