2
头图
Foreword: We start with the React source code, combine the specific business of the front-end of Youdao's fine class, and use three principles to optimize the system surgically. At the same time, I will introduce how the React Profiler tool helps us locate performance bottlenecks. Preface: We start with the React source code, combine the specific business of the front-end of the Youdao boutique class, and use three principles to optimize the system surgically. At the same time introduce how the React Profiler tool helps us locate performance bottlenecks

Author/ An Zengping

Edit/Ein

React performance optimization is a problem that has to be considered in the business iteration process. In most cases, it is because the complexity of the project is not fully considered at the beginning of the project. The user size and technical scenarios of the product are not complicated, then we Performance optimization may not need to be considered in the early stages of the business. But as business scenarios become more complex, performance optimization becomes extremely important.

We started with React source code, combined with the specific business of the big front end of Youdao's fine class, and used optimization techniques to optimize the system surgically. At the same time, introduce how React Profiler, a performance optimization tool, helps us locate performance bottlenecks.

The project codes in this article are all work records in the development project of Youdao large front-end team. If there are any shortcomings, please discuss and exchange in the message area. Refill ❤

Page loading process

  1. Suppose the user opens the page for the first time (no cache), this time the page is completely blank;
  2. After the html and referenced css are loaded, the browser performs for the first rendering ;
  3. React, react-dom, and business code are loaded, and the application is rendered for the first time, or for the first content rendering ;
  4. The code of the application starts to execute, pulls data, performs dynamic import, responds to events, etc. After completion, the page enters the interactive state;
  5. Next, lazyload's pictures and other multimedia content began to be loaded gradually;
  6. Until the other resources of the page (such as error reporting components, dot reporting components, etc.) are loaded, the entire page is loaded.

Let's mainly analyze for React:

React's three directions for rendering performance optimization are also applicable to other software development fields. The three directions are:

  1. Reduce the amount of calculation : In React, it is to reduce the rendering node or reduce the rendering complexity through index;
  2. uses cache : React is to avoid re-rendering (using memo to avoid component re-rendering);
  3. Accurately recalculated range 160af195155a22: In React, the relationship between binding components and state is used to accurately determine the

How to do these three points? We start with the analysis from the characteristics of React itself.

React workflow

React is a declarative UI library, responsible for converting State into a page structure (virtual DOM structure), and then into a real DOM structure, which is then rendered by the browser. When the State changes, React will perform Reconciliation first, and enter the Commit phase immediately after the completion. After Commit is over, the page corresponding to the new State will be displayed.

React's Reconciliation needs to do two things:

  1. Calculate the virtual DOM structure corresponding to the target State.
  2. Find the optimal solution of "modify the virtual DOM structure to the target virtual DOM structure".

React traverses the virtual DOM tree in a depth-first manner. After completing the Render and Diff calculations on a virtual DOM, it calculates the next virtual DOM. The Diff algorithm will record the virtual DOM update methods (such as: Update, Mount, Unmount) to prepare for Commit.

React's Commit also needs to do two things:

  1. Apply the Reconciliation results to the DOM.
  2. Call exposed hooks such as: componentDidUpdate, useLayoutEffect, etc.

Below we will conduct precise analysis for the three optimization directions.

Reduce the amount of calculation

Regarding the above two-stage optimization method of Reconciliation and Commit , I followed the method of reduce the amount of calculation list item uses key attribute ) This process is the focus of optimization, and React internal The Fiber structure and concurrency mode are also reducing the time-consuming blocking of the process. For Commit when executing hooks, developers should ensure that the code in hooks is as light as possible to avoid time-consuming blocking, and should avoid updating components CDM and CDU

list item uses the key attribute

In certain frameworks, the prompts are also very friendly. If you don’t add the key attribute to the list, the console will show you a big red

The system will always remind you to remember to add Key~~

Optimize the Render process

Render process: the stage in which the virtual DOM structure corresponding to the target State is calculated in Reconciliation.

There are currently three ways to trigger the Render process of React components:

  1. forceUpdate、
  2. State update,
  3. The parent component Render triggers the child component Render process.

Optimization skills

PureComponent、React.memo

In the React workflow, if only the parent component undergoes a state update, even if all the Props passed from the parent component to the child component are not modified, the Render process of the child component will be caused.

From the perspective of React's declarative design philosophy, if the Props and State of the sub-components are not changed, then the DOM structure and side effects generated should not be changed. When the sub-component conforms to the declarative design concept, the Render process of the sub-component can be ignored.

PureComponent and React.memo respond to this scenario. PureComponent is a shallow comparison of Props and State of class components, and React.memo is a shallow comparison of Props of functional components.

useMemo, useCallback to achieve stable Props value

If the derived state or function passed to the child component is a new reference every time, then the PureComponent and React.memo optimizations will fail. So you need to use useMemo and useCallback to generate stable values, and combine with PureComponent or React.memo to avoid sub-components from re-Rendering.

useMemo to reduce the time-consuming component Render process

useMemo is a caching mechanism to speed up. When its dependency has not changed, it will not trigger a recalculation. It is generally used in the time-consuming scene of "calculating derived state code", such as traversing a large list for statistical information.

大列表渲染

Obviously the role of useMemo is to cache expensive calculations (to avoid costly calculations during each rendering), and use it in business to control variables to update the table

shouldComponentUpdate

In class components, for example, when you want to add an item of data to an array, the code at that time is likely to be state.push(item) instead of const newState = [...state, item].

In this context, developers at the time often used

shouldComponentUpdate to compare Props in depth, and execute the Render process of the component only when the Props is modified. Nowadays, due to the popularity of data immutability and functional components, such optimization scenarios will no longer appear.

In order to fit the idea of ​​shouldComponentUpdate: when passing props to subcomponents, only what they need must be passed instead of all of them in one brain:

The parameters passed into the sub-component must be guaranteed to be used in the sub-component.

Batch update to reduce the number of renders

In the event callback and life cycle managed by React, setState is asynchronous, while setState is synchronous at other times. The root cause of this problem is that React updates setState in batches during the event callbacks and life cycles it manages, and updates immediately at other times.

When updating setState in batches, executing setState multiple times will only trigger the Render process once. On the contrary, when the setState is updated immediately, the Render process is triggered every time the setState is set, and there is a performance impact.

Suppose there is the following component code, which updates two States after the API request result of getData() is returned.

The component will trigger the Render process of the component after setList(data.list), and then trigger the Render process again after setInfo(data.info), causing performance loss. So how do we solve it:

  1. Combine multiple States into a single State. For example, useState({ list: null, info: null }) replaces the two states of list and info.
  2. Use the unstable_batchedUpdates method officially provided by React to encapsulate multiple setStates into the unstable_batchedUpdates callback.

The modified code is as follows:

Refined rendering stage

Update according to priority and respond to users in a timely manner

Priority update is the reverse operation of batch update. The idea is to respond to user behavior first, and then complete the time-consuming operation. A common scenario is: a Modal pops up on the page. When the user clicks the OK button in the Modal, the code will perform two operations:

  1. Close Modal.
  2. The page processes the data returned by Modal and displays it to the user.

When operation 2 needs to be executed for 500ms, the user will obviously feel the delay from when the button is clicked to when the Modal is closed.

The following is a general implementation method, using the slowHandle function as the callback function when the user clicks the button.

The slowHandle() execution process takes a long time, and the user will obviously feel the page freezes after clicking the button.

If the page is given priority to hide the input box, the user can immediately perceive the page update, and there will be no feeling of lag.

The key point to implement priority updates is to move time-consuming tasks to the next macro task for execution, and respond to user behavior first.

For example, in this example, move setNumbers to the callback of setTimeout, the user can immediately see that the input box is hidden after clicking the button, and will not perceive the page jam. The optimized code in the mhd project is as follows:

Publisher subscribers skip the Render process of intermediate components

React recommends putting public data on the common ancestor of all "components that need this state", but after putting the state on the common ancestor, the state needs to be passed down layer by layer until it is passed to the component that uses the state.

Each state update involves the Render process of the intermediate component, but the intermediate component does not care about the state, and its Render process is only responsible for passing the state to the child components. In this scenario, the state can be maintained in the publisher-subscriber mode, and only the components that care about the state can subscribe to the state, and no intermediate components are required to pass the state.

When the status is updated, the publisher publishes a data update message, only the subscriber component will trigger the Render process, and the intermediate component will no longer execute the Render process.

As long as it is a library of publisher subscriber mode, useContext can be used for this optimization. For example: redux, use-global-state, React.createContext, etc.

The usage in the business code is as follows:

It can be seen from the figure that after optimization, only the renderTable component that uses the public state will be updated. This shows that doing so can greatly reduce the number of renders of the parent component and other renderSon... components (reduce the re-rendering of leaf nodes) .

useMemo returns the virtual DOM to skip the Render process of the component

The useMemo can cache the calculation results. If useMemo returns the virtual DOM of the component, the Render phase of the component will be skipped when the useMemo dependency is unchanged.

This method is similar to React.memo, but has the following advantages over React.memo:

  1. more convenient. React.memo needs to wrap the components once to generate new components. And useMemo only needs to be used where there is a performance bottleneck, without modifying the components.
  2. More flexible. useMemo does not need to consider all the Props of the component, but only the values ​​used in the current scene. You can also use useDeepCompareMemo to make a deep comparison of the used values.

In this example, after the status of the parent component is updated, the child components that do not use useMemo will execute the Render process, and the child components that use useMemo will update as needed. How to use in business code:

Accurately judge the'timing' and'scope' of the update

debounce, throttle optimize frequently triggered callbacks

In the search component, the search callback is triggered when the content in the input is modified. When the component can quickly process the search results, the user will not feel the input delay.

However, in the actual scenario, the list page of the middle and back-end applications is very complicated, and the rendering of the components to the search results will cause page freezes, which obviously affects the user's input experience.

In search scenarios, useDebounce+useEffect is generally used to obtain data.

In the search scenario, you only need to respond to the user's last input, without responding to the user's intermediate input value. Debounce is more suitable. Throttle is more suitable for scenes that need to respond to users in real time, such as dragging to adjust the size or dragging to zoom in and out (such as the resize event of a window).

Lazy loading

In SPA, lazy loading optimization is generally used to jump from one route to another.

It can also be used for complex components that are displayed after user operations, such as pop-up modules (large-data-volume pop-ups) displayed after clicking a button.

In these scenarios, the combination of Code Split has higher benefits. The implementation of lazy loading is through Webpack's dynamic import and React.lazy method.

When implementing lazy loading optimization, not only the loading state must be considered, but also fault-tolerant handling of loading failures is required.

Lazy rendering

Lazy rendering means that the component is rendered when it enters or is about to enter the viewable area. For common components such as Modal/Drawer, the content of the component is rendered when the visible property is true, which can also be considered as an implementation of lazy rendering. The usage scenarios of lazy rendering are:

  1. Components that appear multiple times on the page, and the rendering of the components is time-consuming, or the components contain interface requests. If multiple components with requests are rendered, because the browser limits the number of concurrent requests under the same domain name, it may block requests in other components in the visible area, resulting in delayed display of the content in the visible area.
  2. Components that need to be displayed after user operations. This is the same as lazy loading, but lazy rendering does not need to dynamically load modules, and does not need to consider the handling of loading states and loading failures, which is simpler in implementation.

In the implementation of lazy rendering, it is determined whether the component appears in the visible area with the help of react-visibility-observer dependency:

Virtual list

The virtual list is a special scene of lazy rendering. The components of the virtual list are react-window and react-virtualized, both of which are developed by the same author.

react-window is a lightweight version of react-virtualized, and its API and documentation are more friendly. It is recommended to use react-window, you only need to calculate the height of each item:

If the height of each item changes, you can pass a function to the itemSize parameter.

Therefore, in the development process, when all the data returned by the interface is encountered, it is recommended to use virtual list optimization when it is necessary to prevent this kind of performance bottleneck requirements in advance. Example of use: react-window​react-window.vercel.app

The animation library directly modifies the DOM properties, skipping the render phase of the component

This optimization should not be used in business, but it is still worth learning and can be applied to the component library in the future.

Refer to the animation implementation of react-spring. When an animation is started, every time an animation attribute changes will not cause the component to re-Render, but directly modify the relevant attribute values ​​on the dom:

Avoid updating component State in didMount and didUpdate

This technique is not only applicable to didMount and didUpdate, but also includes willUnmount, useLayoutEffect, and useEffect in special scenarios (when the cDU/cDM of the parent component is triggered, the useEffect of the child component will be called synchronously). This article refers to them collectively as " Submit stage hook".

React workflow commit phase is to execute the commit phase hooks, and their execution will block the browser from updating the page.

If the component State is updated in the commit phase hook function, the component update process will be triggered again, causing twice the time-consuming. The general scenarios for updating the component state in the hook of the commit phase are:

  1. Calculate and update the Derived State of the component. In this scenario, the class component should use hook method instead, and the function component should be replaced by setState when the function is called. After using the above two methods, React will complete the new state and derived state in one update.
  2. According to the DOM information, modify the component state. In this scenario, unless you find a way to not rely on DOM information, the two update processes are indispensable, and you can only use other optimization techniques.

The source code of use-swr uses this optimization technique. When there is cached data in an interface, use-swr will first use the cached data of the interface, and then re-initiate the request when requestIdleCallback is used to obtain the latest data. Simulate a swr:

  1. Its second parameter deps is to re-initiate the request if the parameter changes when the request has parameters.
  2. The fetch function exposed to the caller can deal with active refresh scenarios, such as the refresh button on the page.

If use-swr does not do the optimization, it will trigger revalidation in useLayoutEffect and set the isValidating state to true, causing the update process of the component and causing performance loss.

Tool introduction-React Profiler

React Profiler locates the bottleneck of the Render process

React Profiler is a performance review tool officially provided by React. This article only introduces the author's experience. For detailed user manuals, please refer to the official website documentation.

Note: react-dom 16.5+ only supports profiling in DEV mode, and it can also be supported by a profiling bundle react-dom/profiling in the production environment. Please check how to use this bundle on fb.me/react-profi...

The "Profiler" panel is empty at the beginning. You can click the record button to start the profile:

Profiler only records the rendering process time-consuming

Do not use Profiler to locate performance bottlenecks in non-Render processes

With React Profiler, developers can view the time consuming of the component Render process, but they cannot know the time consuming of the submission phase.

Although there is a Committed at field in the Profiler panel, this field is relative to the recording start time and has no meaning at all.

By experimenting on the React v16 version, Chrome's Performance and React Profiler statistics are turned on at the same time.

As shown in the figure below, in the Performance panel, the Reconciliation and Commit phases took 642ms and 300ms, respectively, while the Profiler panel only shows 642ms:

Turn on "Log Component Update Reason"

Click the gear on the panel, and then check "Record why each component rendered while profiling.", as shown below:

Then click on the virtual DOM node in the panel, and the reason for the component's re-Render will be displayed on the right.

Locate the reason for this Render process

Due to React's Batch Update mechanism, a Render process may involve the status update of many components. So how to locate which component status update is caused by it?

In the virtual DOM tree structure on the left side of the Profiler panel, review each component that has been rendered (not grayed out) from top to bottom.

If the component triggers the Render process due to a State or Hook change, then it is the component we are looking for, as shown below:

Standing on the shoulders of giants

Optimizing Performance React official documentation, the best tutorial, make good use of React's performance analysis tools.

Twitter Lite and High Performance React Progressive Web Apps at Scale See how Twitter is optimized.

-END-


有道AI情报局
788 声望7.9k 粉丝