This article comes from the company’s internal lightning share, slightly embellished share out. The main directions and tips for React performance optimization are discussed. If you feel ok, please like more, encourage me to write more wonderful article 🙏.


React rendering performance is optimized in three ways that can be applied to other areas of software development:

  • Reduce the amount of computation. -> React reduces the number of nodes to render or the complexity of component rendering
  • Take advantage of caching. Use memo to avoid rerendering components in React
  • Precise recalculation range. React is used to bind components and state relationships to determine the exact ‘timing’ and ‘scope’ of updates. Only re-render ‘dirty’ components, or reduce the render scope


directory

  • Reduce render nodes/reduce render computation (complexity)
    • 0️ discount on unnecessary calculations in rendering functions
    • 1️ reduce unnecessary nesting
    • 2️ virtual list
    • 3️ discount
    • 4️ selection of appropriate style scheme
  • Avoid rerendering
    • 0 ️ ⃣ simplify the props
    • 1️ invariant event handler
    • 2️ non-variable data
    • 3 ️ ⃣ simplified state
    • 4 Fine comparison of ️ recompose use
  • Fine rendering
    • Fine rendering of 0️ response data
    • 1️ do not abuse Context
  • extension





Reduce render nodes/reduce render computation (complexity)

First of all, focus on the amount of calculation. Reducing the number of nodes rendering or reducing the amount of calculation of rendering can significantly improve the performance of component rendering.


0️ discount on unnecessary calculations in rendering functions

For example, don’t do array sorting, data conversion, subscribe to events, create event handlers, etc., in render. Render functions should not have too many side effects


1️ reduce unnecessary nesting

Our team is heavily styled- Components users, which we don’t need in most cases, such as purely static style rules and scenarios that require heavy performance optimization. In addition to performance issues, the other thing that bothers us is the node-nesting hell it creates (see figure above).

So we need to choose some tools wisely, such as using native CSS, to reduce the burden of the React runtime.

Generally unnecessary node nesting is the result of abusing higher-order components /RenderProps. So again, ‘use XXX only when necessary’. There are several ways to replace higher-order components /RenderProps, such as using props, React Hooks in preference


2️ virtual list

Virtual lists are common ‘long lists’ and’ complex component trees’ optimizations, which essentially reduce the number of nodes to be rendered.

The virtual list only renders elements visible to the current viewport:

Virtual list rendering performance comparison:

Virtual lists are commonly used in the following component scenarios:

  • Infinite scrolling lists, grid, tables, drop-down lists, spreadsheets
  • Infinitely toggle calendar or carousel chart
  • Large data volumes or infinitely nested trees
  • Chat window, data stream (feed), timeline
  • , etc.

Related component schemes:

  • react-virtualized
  • React – Window more lightweight React – Virtualized, same player
  • More and more

Extension:

  • Creating more efficient React views with windowing
  • Rendering large lists with react-window





3️ discount

The original intention of lazy rendering is essentially the same as virtual tables, meaning that we only render the corresponding nodes when necessary.

As a typical example, we use the Tab component a lot. We don’t need to render all the tabs’ panels at first, but wait until the Tab is activated to lazily render.

There are many scenarios that use lazy rendering, such as tree selectors, modal popovers, drop-down lists, collapsing components, etc.

I’ll leave the specific code examples to the reader.


4️ selection of appropriate style scheme

As shown in THE PERFORMANCE OF STYLED REACT COMPONENTS, this picture is 17 years old, but THE general trend remains THE same.

So the run-time performance of the style can be roughly summed up as CSS > most CSS-in-js > inline Style




Avoid rerendering

Reducing unnecessary re-rendering is also an important aspect of performance optimization for the React component. To avoid unnecessary component rerendering, do two things:

  1. Ensure component purity. That is to control side effects of components that cannot safely cache render results
  2. throughshouldComponentUpdateThe lifecycle function compares the state and props to determine whether to rerender. This is available for function componentsReact.memopackaging

In addition, these measures can help you optimize component re-rendering more easily:


0 ️ ⃣ simplify the props

If a component’s props is too complex, it usually means that the component has violated its “single responsibility” and should be disassembly first. Complex props can also be difficult to maintain, affecting shallowCompare efficiency and making component changes difficult to predict and debug.

Here is a typical example. To determine whether a list item is active, a currently active ID is passed:

This is a very bad design, and all list items are refreshed as soon as the activation ID changes. A better solution is to use a Boolean prop like Actived. Actived now has only two changes, i.e. activation ID changes, and only two components need to be rerendered at most.

The simplified props is easier to understand and improves component cache hit ratio


1️ invariant event handler

① Avoid using event handlers in the form of arrow functions, such as:

<ComplexComponent onClick={evt => onClick(evt.id)} otherProps={values} />
Copy the code

Assuming that the ComplexComponent is a complex PureComponent, using the arrow function, a new event handler is created every time it renders, causing the ComplexComponent to always be re-rendered.

A better way is to use the instance method:

class MyComponent extends Component {
  render() {
    <ComplexComponent onClick={this.handleClick} otherProps={values} />;
  }
  handleClick = (a)= > {
    / *... * /
  };
}
Copy the code


(2) Even now using hooks, I still use useCallback to wrap the event handler, trying to expose a static function to descendant components:

const handleClick = useCallback((a)= > {
  / *... * /} []);return <ComplexComponent onClick={handleClick} otherProps={values} />;
Copy the code

But if a useCallback depends on many states, your useCallback might look like this:

const handleClick = useCallback((a)= > {
  / *... * /
  / / 🤭
}, [foo, bar, baz, bazz, bazzzz]);
Copy the code

This is really unacceptable, and who cares what’s functional and non-functional at this point. Here’s how I handled it:

function useRefProps<T> (props: T) {
  const ref = useRef < T > props;
  // Update props for each render
  useEffect((a)= > {
    ref.current = props;
  });
}

function MyComp(props) {
  const propsRef = useRefProps(props);

  // Now handleClick is always the same
  const handleClick = useCallback((a)= > {
    const { foo, bar, baz, bazz, bazzzz } = propsRef.current;
    // do something} []); }Copy the code


(3) Design Event Props that are easier to handle. Sometimes we are forced to use arrow functions as Event handlers:

<List>
  {list.map(i= > (
    <Item key={i.id} onClick={()= > handleDelete(i.id)} value={i.value} />
  ))}
</List>
Copy the code

The onClick implementation above is a bad one. It carries no information to identify the source of the event, so the closure form is used instead. A better design might look like this:

// onClick passes event source information
const handleDelete = useCallback((id: string) = > {
  /* Delete operation */} []);return (
  <List>
    {list.map(i => (
      <Item key={i.id} id={i.id} onClick={handleDelete} value={i.value} />
    ))}
  </List>
);
Copy the code

What if it’s a third-party component or a DOM component? If not, see if you can pass the data-* attribute:

const handleDelete = useCallback(event= > {
  const id = event.currentTarget.dataset.id;
  /* Delete operation */} []);return (
  <ul>
    {list.map(i => (
      <li key={i.id} data-id={i.id} onClick={handleDelete} value={i.value} />
    ))}
  </ul>
);
Copy the code





2️ non-variable data

Immutable data makes the state predictable and makes shouldComponentUpdate ‘shallow comparisons’ more reliable and efficient. I introduced immutable data in React Component Design Practice Summary 04 – Component Thinking.

Related tools are Immutable. Js, Immer, immutability-Helper, and seamless- Immutable.


3 ️ ⃣ simplified state

Not all states should be in a component’s state. For example, cache data. My rule is that data should only be placed in State if it needs the component to respond to its changes, or if it needs to be rendered into the view. This avoids unnecessary data changes that cause components to be re-rendered.


4 Fine comparison of ️ recompose use

Although recompose says it is no longer updating after hooks came out, it does not affect how we use Recompose to control the shouldComponentUpdate method. For example, it provides the following methods to fine-control which props should be compared:

 /* The same as react. memo */
 pure()
 /* Custom comparison */
 shouldUpdate(test: (props: Object, nextProps: Object) = > boolean): HigherOrderComponent
 /* Compares only the specified key */
 onlyUpdateForKeys( propKeys: Array<string>): HigherOrderComponent
Copy the code

You can extend it even further, such as omitUpdateForKeys ignoring certain keys.





Fine rendering

Refined rendering means that only one data source causes the component to be re-rendered. For example, if A only depends on A data, then A should be rendered only when A data changes, and other state changes should not affect component A.

Part of the reason Vue and Mobx claim to perform well is because of their ‘responsive systems ‘, which allow us to define’ responsive data ‘on which views are re-rendered as the response data changes. Here’s how Vue officials describe it:




Fine rendering of 0️ response data

For the most part, responsive data allows for a fine-grained rendering of the view, but it does not prevent developers from writing inefficient programs. Essentially, components violate the ‘single responsibility’.

For example, we now have A MyComponent that relies on data sources A, B, and C to build A VDOM tree. What’s the problem now? Now if A, B, or C changes, the entire MyComponent will be rerendered:

A better approach would be to have a single responsibility for components that rely on, or ‘isolate,’ responsive data in a refined way. As shown below, A, B, and C have all been extracted from each component, and now A change will only render A component itself, without affecting the parent component and B, C components:




To take a typical example, list rendering:

import React from 'react';
import { observable } from 'mobx';
import { observer } from 'mobx-react-lite';

const initialList = [];
for (let i = 0; i < 10; i++) {
  initialList.push({ id: i, name: `name-${i}`.value: 0 });
}

const store = observable({
  list: initialList,
});

export const List = observer((a)= > {
  const list = store.list;
  console.log(Render 'List');
  return (
    <div className="list-container">
      <ul>
        {list.map((i, idx) => (
          <div className="list-item" key={i.id}>{/* Assuming this is a complex component */} {console.log('render', i.id)}<span className="list-item-name">{i.name} </span>
            <span className="list-item-value">{i.value} </span>
            <button
              className="list-item-increment"
              onClick={()= >{ i.value++; The console. The log (' incrementing '); Increasing}} ></button>
            <button
              className="list-item-increment"
              onClick={()= > {
                if (idx < list.length - 1) {
                  console.log(' shift ');let t = list[idx];
                  list[idx] = list[idx + 1];
                  list[idx + 1] = t;}}} >Move down</button>
          </div>
        ))}
      </ul>
    </div>
  );
});
Copy the code


The above example is a performance problem. Incrementing and shifting a single list-item causes the entire list to be re-rendered:

Can you guess why? For Vue or Mobx, a component’s rendering function is a dependent collection context. If the List rendering function ‘accesses’ all of the List item data, Vue or Mobx will assume that your component is dependent on all of the List items, resulting in a re-rendering of the entire List as soon as the property value of any of the List items changes.

The solution is simple, too, to extract data isolation into a single responsible component. For Vue or Mobx, finer grained components yield higher performance optimizations:

export const ListItem = observer(props= > {
  const { item, onShiftDown } = props;
  return (
    <div className="list-item">{console.log('render', item.id)} {/* Assuming this is a complex component */}<span className="list-item-name">{item.name} </span>
      <span className="list-item-value">{item.value} </span>
      <button
        className="list-item-increment"
        onClick={()= >{ item.value++; The console. The log (' incrementing '); Increasing}} ></button>
      <button className="list-item-increment" onClick={()= >OnShiftDown (item)} > move down</button>
    </div>
  );
});

export const List = observer((a)= > {
  const list = store.list;
  const handleShiftDown = useCallback(item= > {
    const idx = list.findIndex(i= > i.id === item.id);
    if(idx ! = =- 1 && idx < list.length - 1) {
      console.log('shift');
      let t = list[idx];
      list[idx] = list[idx + 1];
      list[idx + 1] = t;
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps} []);console.log(Render 'List');

  return (
    <div className="list-container">
      <ul>
        {list.map((i, idx) => (
          <ListItem key={i.id} item={i} onShiftDown={handleShiftDown} />
        ))}
      </ul>
    </div>
  );
});
Copy the code

The effect is obvious: the list-item increment only re-renders itself; The shift only re-renders the List, since the List items have not changed, so the lower-level list-items do not need to be re-rendered:







1️ do not abuse Context

Context is actually the opposite of reactive data. I’ve seen plenty of instances where the Context API has been misused, and ultimately failed to address the ‘state scope problem’.

The first thing to understand about the Context API is that it can be updated through the react. memo or shouldComponentUpdate. All components that depend on the Context are forceUpdate in full.

Unlike the responsive systems of Mobx and Vue, the Context API does not provide fine-grained detection of which components depend on which states, so the ‘finely rendered’ component pattern described in the previous section becomes an ‘anti-pattern’ in Context.

To summarize, use the Context API to follow the following principles:


  • Explicitly state scoping, Context places only necessary, critical states that are shared by most components. The authentication status is typical

    Here’s a simple example:

    Extension: Context actually has an experimental or non-public observedBits option that can be used to control whether the ContextConsumer needs to update it. See this article

    . However, it is not recommended to use in real projects, and the API is difficult to use, so it is better to use Mobx directly.

  • Subscribe to the Context coarse-grained

    Fine-grained Context subscriptions cause unnecessary re-rendering, so coarse-grained subscriptions are recommended. Subscribing to the Context at the parent level and then passing it to the child via props.


The React Context can cause repeated renderings. The React Context can cause repeated renderings.

<Context.Provider
  value={{ theme: this.state.theme, switchTheme: this.switchTheme }}
>
  <div className="App">
    <Header />
    <Content />
  </div>
</Context.Provider>
Copy the code

The above component rerenders the entire component tree when state changes, leaving the reader to wonder why.

So we usually don’t use context. Provider naked, but wrap it as a separate Provider component:

export function ThemeProvider(props) {
  const [theme, switchTheme] = useState(redTheme);
  return (
    <Context.Provider value={{ theme.switchTheme}} >
      {props.children}
    </Context.Provider>); UseTheme () {return useContext(Context); return useContext(Context); }Copy the code

Now the theme change does not re-render the entire component tree because props. Children are passed in from the outside without any change.

The above code has another, more difficult, pitfall (also mentioned in the official documentation):

export function ThemeProvider(props) { const [theme, switchTheme] = useState(redTheme); Return ({/* 👇 💣 where a new value is created each time the ThemeProvider is rendered (even if the theme and switchTheme are unchanged), Provider value={{theme, switchTheme}}> {props. Children} </ context.provider >); }Copy the code

So the value passed to the Context should be cached:

export function ThemeProvider(props) {
  const [theme, switchTheme] = useState(redTheme);
  const value = useMemo((a)= > ({ theme, switchTheme }), [theme]);
  return <Context.Provider value={value}>{props.children}</Context.Provider>;
}
Copy the code





extension

  • Optimizing Performance Analysis of React
  • Twitter Lite and High Performance React Progressive Web Apps at Scale