Questions answered

By reading this article, you will find answers to the following questions:

  1. What is the React workflow? At what stages can we optimize performance?
  2. What performance optimization techniques can we use if there is a lag in the React project?
  3. How do I locate performance issues with React Profiler? What stages of information does the React Profiler contain?

The outline

This article is divided into three parts. First, the React workflow is introduced to give readers a macro understanding of the React component update process. Then the author summarizes a series of optimization techniques, and for a slightly more complex optimization techniques prepared CodeSandbox source code, so that the reader hands-on experience. Finally, I would like to share some tips on using React Profiler to help readers identify performance bottlenecks faster.

The React workflow

React is a declarative UI library that converts State into a page structure (virtual DOM structure) and then into a real DOM structure for rendering by the browser. When the State changes, React conducts the Reconciliation phase first, and immediately enters the Commit phase. After the Commit phase, the page corresponding to the new State is displayed.

The reconcile phase of React requires two things. 1. Calculate the virtual DOM structure corresponding to the target State. 2. Find the optimal update scheme of “changing the virtual DOM structure to the target virtual DOM structure”. React traverses the virtual DOM tree in a depth-first way, and then calculates the next virtual DOM after completing two calculations on one virtual DOM. The first thing is primarily to call the class component’s Render method or the function component itself. The second thing is the Diff algorithm implemented inside React. The Diff algorithm will record the Update mode of the virtual DOM (such as: Update, Mount, Unmount) to prepare for the submission phase.

The React submission phase also requires two things. 1. Apply the update scheme of reconciliation phase records to DOM. 2. Call the hook methods exposed to the developer, such as componentDidUpdate, useLayoutEffect, etc. The execution time of these two things in the submission phase is different from that in the reconciliation phase. React will execute 1 first in the submission phase, and then execute 2 after the completion of 1. So in the child component’s componentDidMount method, you can execute document.querySelector(‘.parentClass’) to get the.parentClass DOM node rendered by the parent component, Although the parent component’s componentDidMount method has not yet been executed. UseLayoutEffect execution time and componentDidMount is the same, can refer to the online code for verification.

Since the “Diff process” in the reconcile phase and the “apply update scheme to the DOM” in the submit phase are both internal implementations of React, there is only one optimization tip that the developers can provide (using the key attribute for list items). In actual engineering, most optimization methods are focused on the process of “computing target virtual DOM structure” in the harmonization stage, which is the focus of optimization. The Fiber architecture and concurrent mode inside React also reduce the time-consuming blocking in this process. For the “execute hook function” process in the commit phase, the developer should keep the hook function code as light as possible to avoid time-consuming blocking. For optimization tips, refer to this article to avoid updating component State in didMount and didUpdate.

Process knowledge

  1. Readers unfamiliar with the React lifecycle are advised to read this article in conjunction with the React component lifecycle diagram. Remember to check the box on the site.
  2. Since you don’t know when the page will be updated until you understand the event loop, recommend oneIntroduce the event loop video. The pseudocode for the event loop in this video is shown below, which is pretty straightforward to follow.

Define the Render process

For the convenience of description, the process of “computing target virtual DOM structure” in the reconciliation phase is called Render process. There are three ways to trigger the React component’s Render process: forceUpdate, State update, and parent component Render triggering the child component’s Render process.

Optimization techniques

In this paper, optimization techniques are divided into three categories:

  1. Skip unnecessary component updates. This type of optimization is achieved by reducing unnecessary component updates after component state changes, and is a major part of this article’s optimization tips.
  2. Commit phase optimization. The purpose of this type of optimization is to reduce the commit phase time, and there is only one optimization tip in this category.
  3. Front-end general optimization. This type of optimization exists in all front-end frameworks, and the focus of this article is to apply these techniques to the React component.

Skip unnecessary component updates

This type of optimization is achieved by reducing unnecessary component updates after component state changes, and is a major part of this article’s optimization tips.

1. PureComponent, React. Memo

In the React workflow, if only the parent component has a status update, it causes the child component’s Render process even if all Props passed by the parent to the child component have not been modified. React’s declarative design philosophy dictates that if the Props and states of a child component do not change, then the GENERATED DOM structure and side effects should not change either. When the child component conforms to the declarative design concept, the child component can ignore the Render process. PureComponent and React.memo are for this scenario. PureComponent is a light comparison of Props and State for class components, and React.memo is a light comparison of functions.

2. shouldComponentUpdate

In the early days of React, data immutability wasn’t as popular as it is today. Instead of using expansion syntax to generate new object references, the Flux schema uses a module variable to maintain State and directly changes the attribute values of that module variable when the State is updated. For example, to add an item to an array, the code would probably be state.push(item) rather than const newState = […state, item]. Consider the Flux code Dan Abramov demonstrated during his Redux presentation.

In this context, developers at the time often used shouldComponentUpdate to compare Props deeply, executing the component Render procedure only if there were changes to the Props. Today, due to data immutability and the popularity of function components, such optimization scenarios are no longer available.

Here’s another scenario you can optimize using shouldComponentUpdate. At the beginning of a project, developers often pass a large Props object to the child component as a convenience, and then use whatever the backend component wants. The child component also triggers the Render process when an “unused property of a child component” in a large object is updated. In this scenario, you can prevent the child component from rerender by implementing its shouldComponentUpdate method, which returns true only if “properties used by the child component” have changed.

There are two disadvantages to using shouldComponentUpdate to optimize the second scenario.

  1. If there are many descendant components, “finding the properties used by all the descendant components” can be a lot of work and can lead to bugs due to missed tests.
  2. There are potential engineering risks. For example, suppose the component structure is as follows.
<A data="{data}">
  {/* the B component uses only data.a and data. B */}
  <B data="{data}">
    {/* The C component only uses data.a */}
    <C data="{data}"></C>
  </B>
</A>
Copy the code

B component shouldComponentUpdate only compares the data. A and data. B, is now no any problem. The developer then wants to use data.c in the C component, assuming that data.a and data.c are updated together in the project, so there is no problem. But this code is already fragile, and if a change causes data.a and data.c not to be updated together, then the system will have problems. And the actual business code is often more complex, from B to C may have a number of intermediate components, then it is difficult to think of shouldComponentUpdate caused by the problem.

Process knowledge

  1. The best solution for the second scenario is to use the publisher subscriber mode, but with slightly more code changes. See the optimization tip “Publisher subscriber skips the intermediate component Render process” in this article.
  2. The second scenario can also add an intermediate component between the parent component and the child component. The intermediate component is responsible for selecting the properties that the child component cares about from the parent component and passing them to the child component. This adds the component layer to the shouldComponentUpdate method, but does not have the second disadvantage.
  3. The Render procedure triggered by the change to skip the callback in this article can also be implemented with shouldComponentUpdate because the callback function does not participate in the component’s Render procedure.

3. UseMemo, useCallback implements stable Props

The PureComponent and React.memo optimizations are invalidated if the derived state or function passed to the child component is a new reference each time. Use useMemo and useCallback to generate stable values, and use PureComponent or React.memo to avoid rerender of child components.

Process knowledge

UseCallback is a special case where “useMemo returns a function,” which is a handy way to do it with React. In the React Server Hooks code, useCallback is implemented based on useMemo. Although the React Client Hooks do not use the same code, the code logic of useCallback is the same as that of useMemo.

4. Publisher subscribers skip the intermediate component Render process

React recommends putting public data on the common ancestor of all “components that need the state,” but by putting the state on the common ancestor, the state needs to be passed down the hierarchy 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, its Render process is only responsible for passing the state to the child components. In this scenario, the state can be maintained as a publisher subscriber pattern, where only components that care about the state subscribe to the state, and no intermediate component is required to deliver the state. When the status updates, the publisher publishes the data update message, only the subscriber component triggers the Render process, the intermediate component no longer performs the Render process.

This optimization can be done for any library in the publisher – subscriber mode. Example: redux, use-global-state, and React.createContext. Example reference: The publisher subscriber mode skips the rendering phase of the intermediate component, which is implemented in this example using React.createcontext.

import { useState, useEffect, createContext, useContext } from "react"

const renderCntMap = {}
const renderOnce = name= > {
  return (renderCntMap[name] = (renderCntMap[name] || 0) + 1)}// Move the parts that require public access to the Context for optimization
// Context.Provider is the publisher
// Context.Consumer
const ValueCtx = createContext()
const CtxContainer = ({ children }) = > {
  const [cnt, setCnt] = useState(0)
  useEffect(() = > {
    const timer = window.setInterval(() = > {
      setCnt(v= > v + 1)},1000)
    return () = > clearInterval(timer)
  }, [setCnt])

  return <ValueCtx.Provider value={cnt}>{children}</ValueCtx.Provider>
}

function CompA({}) {
  const cnt = useContext(ValueCtx)
  // Use CNT in component
  return <div>{Render once ("CompA")}</div>
}

function CompB({}) {
  const cnt = useContext(ValueCtx)
  // Use CNT in component
  return <div>{Render once ("CompB")}</div>
}

function CompC({}) {
  return <div>{Render once ("CompC")}</div>
}

export const PubSubCommunicate = () = > {
  return (
    <CtxContainer>
      <div>
        <h1>Optimized scene</h1>
        <div>Elevate the state to the upper level of the lowest common ancestor and wrap its contents in CtxContainer.</div>
        <div style={{ marginTop: "20px}} ">Each time we Render, only components A and B will re-render.</div>

        <div style={{ marginTop: "40px}} ">{Render once ("parent")}</div>
        <CompA />
        <CompB />
        <CompC />
      </div>
    </CtxContainer>)}export default PubSubCommunicate
Copy the code

5. Delegate the status to reduce the scope of state influence

If a state is only used in one part of the molecular tree, then the tree can be extracted as a component and the state can be moved inside that component. and

and < sivetree /> can be rerendered for ExpensiveTree.

import { useState } from "react"

export default function App() {
  let [color, setColor] = useState("red")
  return (
    <div>
      <input value={color} onChange={e= > setColor(e.target.value)} />
      <p style={{ color}} >Hello, world!</p>
      <ExpensiveTree />
    </div>)}function ExpensiveTree() {
  let now = performance.now()
  while (performance.now() - now < 100) {
    // Artificial delay -- do nothing for 100ms
  }
  return <p>I am a very slow component tree.</p>
}
Copy the code

By extracting the color state, , and

into the component Form, the result is as follows.

export default function App() {
  return (
    <>
      <Form />
      <ExpensiveTree />
    </>)}function Form() {
  let [color, setColor] = useState("red")
  return (
    <>
      <input value={color} onChange={e= > setColor(e.target.value)} />
      <p style={{ color}} >Hello, world!</p>
    </>)}Copy the code

After this adjustment, the color change will not cause component App and ExpensiveTree to Render again.

If you extend the scenario above and use state color in both the top level and subtree of the component App, but
still doesn’t care about it, as shown below.

import { useState } from "react"

export default function App() {
  let [color, setColor] = useState("red")
  return (
    <div style={{ color}} >
      <input value={color} onChange={e= > setColor(e.target.value)} />
      <ExpensiveTree />
      <p style={{ color}} >Hello, world!</p>
    </div>)}Copy the code

In this scenario, we still extract the color state into the new component and provide a slot to combine
, as shown below.

import { useState } from "react"

export default function App() {
  return <ColorContainer expensiveTreeNode={<ExpensiveTree />} ></ColorContainer>
}

function ColorContainer({ expensiveTreeNode }) {
  let [color, setColor] = useState("red")
  return (
    <div style={{ color}} >
      <input value={color} onChange={e= > setColor(e.target.value)} />
      {expensiveTreeNode}
      <p style={{ color}} >Hello, world!</p>
    </div>)}Copy the code

After this adjustment, the color change will not cause component App and ExpensiveTree to Render again.

This optimization tip comes from before-you-memo. Dan thinks this optimization works better in the Server Component scenario because < siveTree /> can be performed on the Server side.

6. The list item uses the key attribute

When rendering a list item, if you do not set the component to an unequal property key, you will receive the following alarm.

Many developers have seen this warning hundreds of times, so what’s the key attribute optimizing for? For example 🌰, the result of the component Render twice without a key is as follows.

<! Render result -->
<ul>
  <li>Duke</li>
  <li>Villanova</li>
</ul>

<! -- New Render results -->
<ul>
  <li>Connecticut</li>
  <li>Duke</li>
  <li>Villanova</li>
</ul>
Copy the code

At this time, React Diff algorithm will compare the first two

  • and create Villanova li according to the order in which the
  • appeared. It will perform two DOM updates and one DOM creation.
  • If the React key attribute is added, the result of the two Render results is as follows.

    <! Render result -->
    <ul>
      <li key="2015">Duke</li>
      <li key="2016">Villanova</li>
    </ul>
    
    <! -- New Render results -->
    <ul>
      <li key="2014">Connecticut</li>
      <li key="2015">Duke</li>
      <li key="2016">Villanova</li>
    </ul>
    Copy the code

    The React Diff algorithm will compare the virtual DOM whose key value is 2015, and find that the virtual DOM whose key value is 2015 has not been modified, so there is no need to update. Similarly, the virtual DOM with key 2016 does not need to be updated. As a result, you simply create the virtual DOM with key 2014. Using a key saves two DOM updates compared to code that doesn’t.

    If you replace the

  • in the example with a custom component, and the custom component uses PureComponent or React.memo optimization. Using the key attribute saves not only DOM updates, but also the component’s Render process.
  • React officially recommends that the ID of each data be used as the key of the component to achieve the above optimization purpose. And it is not recommended to use the index of each item as the key, because passing the index as the key degrades the code without the key. Is ID better than index in all list rendering scenarios?

    The answer is no. In a common paging list, the list item ID on the first and second pages is different. Assuming that each page shows three pieces of data, the Render result of the component before and after switching pages is as follows.

    <! -- Page 1 list item virtual DOM -->
    <li key="a">dataA</li>
    <li key="b">dataB</li>
    <li key="c">dataC</li>
    
    <! -- Switch to virtual DOM after page 2 -->
    <li key="d">dataD</li>
    <li key="e">dataE</li>
    <li key="f">dataF</li>
    Copy the code

    After switching to the second page, the Diff algorithm marks all DOM nodes on the first page as deleted, and then marks all DOM nodes on the second page as added, because the key values of all

  • are different. The entire update process requires three DOM deletions and three DOM creation. Without a key, the Diff algorithm simply marks three
  • nodes as updates and performs three DOM updates. Refer to the page list without adding, deleting, and sorting functions in Demo. Each page turning takes about 140ms when using keys, but only 70ms when using keys.
  • Despite the above scenarios, React officials still recommend using ID as the key value for each item. There are two reasons for this:

    1. When deleting, inserting, or sorting items in a list, it is more efficient to use the ID as the key. However, the page turning operation is often accompanied by API request, and the TIME of DOM operation is much less than that of API request. Whether to use ID has little impact on user experience in this scenario.
    2. Using the ID as the key maintains the State of the list item component corresponding to that ID. For example, a table has two states for each column: normal and edit. At first, all columns are normal, and the user clicks on the first column of the first row to make it enter edit mode. The user then drags the second row to move it to the first row of the table. If the developer uses the index as the key, the first row and first column are still in the edit state, but the user actually wants to edit the second row, which is not expected by the user. Although this problem can be solved by using Props to store “is it in edit mode” in the data item’s data, wouldn’t it be better to use the ID as the key?

    7. The useMemo returns the virtual DOM

    Taking advantage of useMemo’s ability to cache computed results, if useMemo returns a component’s virtual DOM, the Render phase of the component will be skipped while useMemo dependencies remain. This method is similar to react. memo, but has the following advantages over react. memo:

    1. It is more convenient. React. Memo needs to wrap the component once to generate a new component. UseMemo, on the other hand, needs to be used only where there are performance bottlenecks, without modifying components.
    2. More flexible. UseMemo doesn’t need to consider all Props for a component, but only the values used in the current scenario. You can also use useDeepCompareMemo to do a deep comparison of the values used.

    Example reference: useMemo skips the component Render process. In this example, the child component that does not use useMemo executes the Render process after the parent component status update, while the child component that uses USemo does not.

    import { useEffect, useMemo, useState } from "react"
    import "./styles.css"
    
    const renderCntMap = {}
    
    function Comp({ name }) {
      renderCntMap[name] = (renderCntMap[name] || 0) + 1
      return (
        <div>{Render cntmap [name]}</div>)}export default function App() {
      const setCnt = useState(0) [1]
      useEffect(() = > {
        const timer = window.setInterval(() = > {
          setCnt(v= > v + 1)},1000)
        return () = > clearInterval(timer)
      }, [setCnt])
    
      const comp = useMemo(() = > {
        return <Comp name="Using useMemo as children" />
      }, [])
    
      return (
        <div className="App">
          <Comp name="Directly as children" />
          {comp}
        </div>)}Copy the code

    8. Skip the callback function changes that trigger the Render procedure

    Props for the React component fall into two categories. A) Properties that affect component Render, such as page data, getPopupContainer, and renderProps. B) Another type is the callback function after component Render, such as onClick, onVisibleChange. B) class attributes do not participate in the component Render process because b) class attributes can be optimized. Instead of triggering a rerender of the component when a b) class property changes, the latest callback function is called when the callback fires.

    In A Complete Guide to useEffect, Dan Abramov argues that having its own event callback for each Render is A cool feature. However, this feature requires the component to be rerendered every time the callback function changes, which is a tradeoff during performance tuning.

    Example reference: Skip the callback function changes that trigger the Render procedure. Demo through the interception of sub-components Props to achieve, just because the author is relatively lazy do not want to change, this way of implementation can also broaden the reader’s vision. This optimization idea should actually be implemented using useMemo/React.memo, and is easier to understand when implemented using useMemo.

    import { Children, cloneElement, memo, useEffect, useRef } from "react"
    import { useDeepCompareMemo } from "use-deep-compare"
    import omit from "lodash.omit"
    
    let renderCnt = 0
    
    export function SkipNotRenderProps({ children, skips }) {
      if(! skips) {// All callbacks are skipped by default
        skips = prop= > prop.startsWith("on")}const child = Children.only(children)
      const childProps = child.props
      const propsRef = useRef({})
      const nextSkippedPropsRef = useRef({})
      Object.keys(childProps)
        .filter(it= > skips(it))
        .forEach(key= > {
          // The proxy function is generated only once, and its value is always the same
          nextSkippedPropsRef.current[key] =
            nextSkippedPropsRef.current[key] ||
            function skipNonRenderPropsProxy(. args) {
              propsRef.current[key].apply(this, args)
            }
        })
    
      useEffect(() = > {
        propsRef.current = childProps
      })
    
      // Use the useMemo optimization technique
      // Remove the callback function, and the other property changes generate a new React.Element
      return useShallowCompareMemo(() = > {
        returncloneElement(child, { ... child.props, ... nextSkippedPropsRef.current, }) }, [omit(childProps,Object.keys(nextSkippedPropsRef.current))])
    }
    
    // The SkipNotRenderPropsComp component has the same content as the Normal component
    export function SkipNotRenderPropsComp({ onClick }) {
      return (
        <div className="case">
          <div className="caseHeader">Skip the "Props not related to Render" changes triggered by re-render</div>{++renderCnt}<div>
            <button onClick={onClick} style={{ color: "blue}} ">Click "I Callback" and the pop-up value of the callback is 1000 (optimized successfully).</button>
          </div>
        </div>)}export default SkipNotRenderPropsComp
    Copy the code

    9. Hooks are updated as needed

    If a custom Hook exposes multiple states, and the caller only cares about one state, then other state changes should not trigger the component to rerender.

    export const useNormalDataHook = () = > {
      const [data, setData] = useState({ info: null.count: null })
      useEffect(() = > {
        const timer = setInterval(() = > {
          setData(data= > ({
            ...data,
            count: data.count + 1,}})),1000)
    
        return () = > {
          clearInterval(timer)
        }
      })
    
      return data
    }
    Copy the code

    As shown above, useNormalDataHook exposes two states info and count to the caller. If the caller only cares about the INFO field, then the count change does not necessarily trigger the caller component Render.

    On-demand updates are implemented in two main steps, refer to Hooks for on-demand updates

    1. Dependency collection based on the data used by the caller, used in DemoObject.definePropertiesThe implementation.
    2. Component updates are triggered only when dependencies change.

    10. The animation library directly modifies DOM properties

    This optimization should not be useful in the business, but is well worth learning and can be applied to component libraries in the future. Refer to the react-Spring animation implementation. When an animation is started, each change in the animation property does not cause the component to rerender, but directly changes the value of the relevant property on the DOM.

    Example Demo: CodeSandbox online Demo

    import React, { useState } from "react"
    import { useSpring, animated as a } from "react-spring"
    import "./styles.css"
    
    let renderCnt = 0
    export function Card() {
      const [flipped, set] = useState(false)
      const { transform, opacity } = useSpring({
        opacity: flipped ? 1 : 0.transform: `perspective(600px) rotateX(${flipped ? 180 : 0}deg)`.config: { mass: 5.tension: 500.friction: 80}})The values of opacity and Transform change continuously during the animation
      // But there is no rerender of the component
      return (
        <div onClick={()= >set(state => ! state)}><div style={{ position: "fixed", top: 10.left: 10}} >Render times: {++renderCnt}</div>
          <a.div
            class="c back"
            style={{ opacity: opacity.interpolate(o= > 1 - o), transform }}
          />
          <a.div
            class="c front"
            style={{
              opacity.transform: transform.interpolate(t= > `${t} rotateX(180deg)`),
            }}
          />
        </div>)}export default Card
    Copy the code

    Commit phase optimization

    The purpose of this type of optimization is to reduce the commit phase time, and there is only one optimization tip in this category.

    1. Avoid updating component State in didMount and didUpdate

    This technique applies not only to didMount and didUpdate, but also to willUnmount, useLayoutEffect, and useEffect in special scenarios (when the parent’s cDU/cDM is triggered, the child’s useEffect is called synchronously). This article refers to them collectively as “commit phase hooks” for the sake of presentation.

    The second step in the React workflow commit phase is the execution of commit phase hooks that block the browser update page. If the component State is updated in the commit phase hook function, the update process of the component will be triggered again, resulting in twice the time.

    Common scenarios for updating a component’s state in a commit phase hook are:

    1. Compute and update the Derived State of a component. In this scenario, the class component should be replaced with the hook method getDerivedStateFromProps, and the function component should be replaced by executing setState on the function call. With the two methods above, React completes the new state and derived state in a single update.
    2. Modify the component state based on DOM information. In this scenario, unless you find a way not to rely on DOM information, the two-update process will be necessary, and other optimization techniques will be used.

    The source code for use-SWR uses this optimization technique. If an interface has cached data, use-SWR first uses the cached data of the interface and then resends the request after requestIdleCallback to obtain the latest data. If use-SWR does not do this optimization, it will trigger a revalidation in useLayoutEffect and set the isValidating state to true, causing the component’s update process to take effect, resulting in a performance penalty.

    General front-end optimization

    This type of optimization exists in all front-end frameworks, and the focus of this article is to apply these techniques to the React component.

    1. Mount components as required

    Component mount optimization on demand can be divided into lazy load, lazy render, and virtual list.

    Lazy loading

    In SPA, lazy load optimization is typically used to jump from one route to another. It can also be used for complex components that are displayed after the user has acted on them, such as pop-up modules that are displayed after clicking a button (sometimes a pop-up is a complex page 😌). In these scenarios, combining Code Split yields higher returns.

    Lazy loading is implemented through Webpack’s dynamic import and the React. Lazy method,

    For example, lazy-loading. When lazy load optimization is implemented, not only load states should be considered, but also fault-tolerant processing for load failures should be carried out.

    import { lazy, Suspense, Component } from "react"
    import "./styles.css"
    
    // Error tolerance for load failures
    class ErrorBoundary extends Component {
      constructor(props) {
        super(props)
        this.state = { hasError: false}}static getDerivedStateFromError(error) {
        return { hasError: true}}render() {
        if (this.state.hasError) {
          return <h1>This handles the error scenario</h1>
        }
    
        return this.props.children
      }
    }
    
    const Comp = lazy(() = > {
      return new Promise((resolve, reject) = > {
        setTimeout(() = > {
          if (Math.random() > 0.5) {
            reject(new Error("Analog network error"))}else {
            resolve(import("./Component"}})),2000)})})export default function App() {
      return (
        <div className="App">
          <div style={{ marginBottom: 20}} >When lazy load optimization is implemented, not only load states should be considered, but also fault-tolerant processing for load failures should be carried out.</div>
          <ErrorBoundary>
            <Suspense fallback="Loading...">
              <Comp />
            </Suspense>
          </ErrorBoundary>
        </div>)}Copy the code

    Lazy rendering

    Lazy rendering refers to rendering a component when it is in or about to enter the viewable area. Common components such as Modal/Drawer, which render component contents only when the visible attribute is true, can also be considered as an implementation of lazy rendering.

    The scenarios for lazy rendering are:

    1. Components that appear multiple times in a page, that take time to render, or that contain interface requests. If you render multiple components with requests, because the browser limits the number of concurrent requests under the same domain name, you may block requests from other components in the visible region, resulting in delayed display of the content in the visible region.
    2. Components that are displayed only after user operations. This is the same as lazy loading, but lazy rendering is easier to implement without dynamically loading modules and without having to worry about loading states and load failures.

    The implementation of lazy rendering to determine whether a component is in the visible area is monitored using react-visibility observers.

    Example reference: lazy rendering

    import { useState, useEffect } from "react"
    import VisibilityObserver, {
      useVisibilityObserver,
    } from "react-visibility-observer"
    
    const VisibilityObserverChildren = ({ callback, children }) = > {
      const { isVisible } = useVisibilityObserver()
      useEffect(() = > {
        callback(isVisible)
      }, [callback, isVisible])
    
      return <>{children}</>
    }
    
    export const LazyRender = () = > {
      const [isRendered, setIsRendered] = useState(false)
    
      if(! isRendered) {return (
          <VisibilityObserver rootMargin={"0px 0px 0px 0px"} >
            <VisibilityObserverChildren
              callback={isVisible= > {
                if (isVisible) {
                  setIsRendered(true)
                }
              }}
            >
              <span />
            </VisibilityObserverChildren>
          </VisibilityObserver>)}console.log("Render only if you scroll to the visible area.")
      return <div>I am the LazyRender component</div>
    }
    
    export default LazyRender
    Copy the code

    Virtual list

    Virtual lists are a special type of scene that can be lazily rendered. The virtualized list has react-Window and React-Virtualized components, both developed by the same author. React-window is a light weight version of React-Virtualized and it is API and document friendly. And then they recommend react-window instead of Star which is more React-virtualized.

    Using React-window is easy. You just need to calculate the height of each item. The height of each item in the following code is 35px.

    Example reference: Official example

    import { FixedSizeList as List } from "react-window"
    const Row = ({ index, style }) = > <div style={style}>Row {index}</div>
    
    const Example = () = > (
      <List
        height={150}
        itemCount={1000}
        itemSize={35}// The height of each term is35
        width={300}
      >
        {Row}
      </List>
    )
    Copy the code

    If the height of each item changes, pass a function to the itemSize argument.

    For this optimization point, the author encountered a real case. In the company’s recruitment program, you can view all the post records of a candidate through a drop-down menu. Normally this list is only a few dozen, but users have reported that “the drop-down menu takes a long time to display the delivery list.” The reason for this problem is that this candidate has thousands of posts in our system, and displaying thousands of posts at once causes the page to get stuck. Therefore, in the development process, when the interface returns all data, it is necessary to prevent such bugs in advance and use virtual list optimization.

    2. Batch update

    Let’s recall an old React interview question from a few years ago. Is setState synchronized or asynchronous in a React component? If you’re not familiar with the class component, think of setState as the second return value of useState.

    balabala…

    The answer is: the setState is asynchronous during React managed event callbacks and lifecycles, while the setState is synchronous at other times. The root cause of this problem is that React updates setStates in bulk in its own managed event callbacks and lifecycle, whereas at other times it updates them immediately. Readers can check whether setState is synchronous or asynchronous by referring to the online example.

    When the setState is updated in batches, executing setState multiple times will only trigger the Render procedure once. Conversely, when the setState is updated immediately, the Render procedure is triggered once for each setState and there is a performance impact.

    Suppose you have code for a component that updates two states after the result of the API request from getData() is returned. Online code practical reference: batchUpdates batchUpdates.

    function NormalComponent() {
      const [list, setList] = useState(null)
      const [info, setInfo] = useState(null)
    
      useEffect(() = > {
        ;(async() = > {const data = await getData()
          setList(data.list)
          setInfo(data.info)
        })()
      }, [])
    
      return <div>{Render once ("normal")}</div>
    }
    Copy the code

    This component triggers the component’s Render procedure after setList(data.list), and then again after setInfo(data.info), causing a performance penalty. There are two ways for developers to implement batch updates to solve this problem:

    1. Merge multiple states into a single State. For example, usingconst [data, setData] = useState({ list: null, info: null })Replace the list and info states.
    2. Use the React unstable_batchedUpdates method to encapsulate multiple setStates into the unstable_batchedUpdates callback. The modified code is as follows:
    function BatchedComponent() {
      const [list, setList] = useState(null)
      const [info, setInfo] = useState(null)
    
      useEffect(() = > {
        ;(async() = > {const data = await getData()
          unstable_batchedUpdates(() = > {
            setList(data.list)
            setInfo(data.info)
          })
        })()
      }, [])
    
      return <div>{Render once ("batched")}</div>
    }
    Copy the code

    Process knowledge

    1. Recommended Reading Why is setState asynchronous?
    2. Why wouldn’t an interviewer ask “is setState in a function component synchronous or asynchronous?” ? Because the generated function in the function component refers to state through the closure, rather than through this.state, the state in the function component’s handler must be the old value, not the new value. The function component has sort of blocked that question out, so interviewers don’t ask it. See the online example.
    3. According to theThe official documentationIn React concurrent mode, setState will be executed as batch updates by default. At that point, the optimization may not be needed.

    3. Update by 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 before completing time-consuming operations.

    A common scenario is that a Modal pops up on the page, and when the user clicks the OK button in Modal, the code performs two actions. A) close Modal. B) The page processes the data returned by Modal and presents it to the user. When the b) operation needs to be executed for 500ms, the user will obviously feel the delay between clicking the button and Modal being closed.

    Example: CodeSandbox online Demo. In this example, after the user adds an integer, the page hides the input field and adds the newly added integer to the list of integers, sorting the list before displaying it. Here’s a general implementation, using the slowHandle function as a callback to the user’s button click.

    const slowHandle = () = > {
      setShowInput(false)
      setNumbers([...numbers, +inputValue].sort((a, b) = > a - b))
    }
    Copy the code

    SlowHandle () takes a long time to execute, and when the user clicks on the button, the page freezes noticeably. If the page hides the input box first, the user will be aware of the page updates immediately, without feeling stuck. The key to implementing priority update is to move the time-consuming task to the next macro task, responding to user behavior first. For example, in this case, by moving setNumbers into the callback to setTimeout, the user clicks the button and immediately sees that the input box is hidden, without being aware that the page is stuck. The optimized code is as follows.

    const fastHandle = () = > {
      // Respond to user behavior preferentially
      setShowInput(false)
      // Move the time-consuming task to the next macro task
      setTimeout(() = > {
        setNumbers([...numbers, +inputValue].sort((a, b) = > a - b))
      })
    }
    Copy the code

    4. Optimize the cache

    Cache optimization is often the simplest and most efficient way to optimize. In React components, useMemo is used to cache the results of the last calculation. When the useMemo dependency has not changed, the recalculation will not be triggered. Usually used in scenarios where “code to calculate derived state” is time consuming, such as traversing a large list for statistics.

    Process knowledge

    1. React officials do not guarantee that useMemo will cache, so it is possible to perform recalculation even if the dependencies do not change. Refer to How to Memoize Calculations
    2. UseMemo can only cache the results of the most recent function execution. If you want to cache the results of more function executions, you can use Memoizee.

    5. Optimize callbacks that are frequently triggered by debounce and Throttle

    In the search component, the search callback is triggered when the content in the input changes. When the component can process the search results quickly, the user does not feel the input delay. However, in the actual scenario, the list page of the background application is very complex, and the Render of the search results by components will cause the page lag, which obviously affects the user’s input experience.

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

    Example reference: debounce-search.

    import { useState, useEffect } from "react"
    import { useDebounce } from "use-debounce"
    
    export default function App() {
      const [text, setText] = useState("Hello")
      const [debouncedValue] = useDebounce(text, 300)
    
      useEffect(() = > {
        // Search by debouncedValue
      }, [debouncedValue])
    
      return (
        <div>
          <input
            defaultValue={"Hello"}
            onChange={e= > {
              setText(e.target.value)
            }}
          />
          <p>Actual value: {text}</p>
          <p>Debounce value: {debouncedValue}</p>
        </div>)}Copy the code

    Why is Debounce used in the search scenario instead of Throttle?

    For details, see useThrottleCallback. For all throttle throttle is a special scenario for Debounce, the maxWait parameter is passed to Debounce. Debounce is more suitable for search scenarios where you only need to respond to the user’s last input rather than the intermediate input value. Throttle is more suitable for scenarios that need to respond to users in real time, such as adjusting size by dragging or zooming in and out by dragging (such as window resize events). In real-time response user scenarios, you can even use a requestAnimationFrame instead of throttle if the callback takes less time.

    React Profiler locates the Render process bottleneck

    React Profiler is an official performance review tool provided by React. This article only introduces the author’s experience in using the React Profiler. For detailed user manual, please refer to the official website document.

    Profiler only records the time consuming of the Render process

    React Profiler allows developers to see how long the component Render process takes, but not how long the commit phase takes. Although there is a Committed at field in the Profiler panel, this field is relative to the recording start time and is not meaningful at all. Readers are cautioned not to use profilers to locate performance bottlenecks in non-Render processes.

    Turn on Performance and React Profiler statistics for Chrome by experimenting with React V16. As shown in the figure below, in the Performance panel, the reconcile phase and the commit phase took 642ms and 300ms respectively, while the Profiler panel shows only 642ms without the commit phase.

    Process knowledge

    1. React has removed User Timing statistics after v17, please refer to PR#18417 for specific reasons.
    2. For the V17 release, I also tested the code to verify that the statistics in the Profiler do not include the commit phase, for those interested.

    Enable Record Component Update Reasons

    On the panel, click on the gear and select “Record why each component rendered while profiling.”

    Then click on the virtual DOM node in the palette, and the reason why the component is rerender is shown on the right.

    Locate the cause of the Render process

    Due to React’s Batch Update mechanism, producing a Render process can involve status updates for many components. So how do you locate which component status updates are causing this?

    In the virtual DOM tree structure on the left side of the Profiler panel, review each of the rendered (non-graying) components from top to bottom. If a component triggers the Render process due to a State or Hook change, it is the component we are looking for, as shown below.

    conclusion

    In this article, we have first mastered the React workflow, a React component status update is divided into reconcile phase and commit phase. The performance optimization of React project is mainly focused on the reconciliation phase, which is achieved by avoiding unnecessary components from entering the Render process as much as possible. In real projects, if performance problems are encountered, React Profiler should be used to locate the time consuming reasons, and then optimization methods should be selected according to the specific situation.

    Selective optimization techniques

    1. If a component enters the Render process because of unnecessary updates, then choose to optimize by skipping unnecessary component updates.
    2. If it’s because the page is mounted with too many invisible components, choose lazy loading, lazy rendering, or virtual list optimization.
    3. If multiple status updates are caused by multiple status Settings, select batch update or callbacks frequently triggered by debounce and Throttle optimization.
    4. If the component Render logic is really time consuming, we need to locate the time consuming code and determine if we can optimize it through caching. If yes, select cache optimization. Otherwise, select priority update to respond to users in a timely manner and disassemble component logic for faster response to users.

    conclusion

    I started to write this article years ago, and it had been a month since it was published. During this period, I added my understanding of React in recent years into the article, adjusted the wording and enriched the examples, and finally completed it before Thursday (Thursday is the deadline 😌 I set).

    Since I put so much effort into it, let’s hope it turns out to be a good React optimization manual.


    If you have questions about this article or have more optimization tips, please feel free to comment.