React code optimizations

Reference:

  1. Avoid the React Context leads to repeated rendering: zhuanlan.zhihu.com/p/50336226
  2. Immutability in the React: There ‘s nothing wrong with mutating objects: blog.logrocket.com/immutabilit…
  3. Immutable. Js, persistent data structures and structure sharing – why use Immutable. Js instead of plain JavaScript objects? : medium.com/@dtinth/imm…
  4. How does Immutable. Js persist data structures? : www.cnblogs.com/DM428/p/139…

directory

  1. Use the React optimization method itself:PureComponent, shouldComponentUpdate, Memo, useMemo, useCallback
  2. React can affect performance:
  1. Using context incorrectly causes all child re-render components, whether or not they are consumers of the context;
  2. Because the React component defaults to policies that update functions based on shallow comparisons, reference types of props cause child components to update unnecessarily along with parent components.
  3. Multiple calls outside of the React component lifecycle method and React event callbacksetState, resulting insetStateCannot be batch, resulting in multiple re-renders
  1. Immutable data,immutable.js,immer
  2. Reduce double counting:reselector

The body of the

1. Use the React optimization method

The React component is updated from the following sources:

  • Update your own state
  • Update of the parent component (whenever the parent component is updated, the current component is forced to update, regardless of whether the props are updated or not)
  • Props to update
  • forceUpdateForced to update

In class components, shouldComponentUpdate returns true by default, and the component is updated whenever any of these four things happen. Before three o ‘clock may cause extra unnecessary updates, in order to avoid unnecessary updates, you can use shouldComponentUpdate state between the old and the new and the old and new props were compared to determine whether components should be updated, even simply use PureComponent, Its internal shouldComponentUpdate is simply a shallow comparison between state and props (so when state/props is an object, if we want to update the component, we have to overwrite the value with a new object, otherwise the component cannot be updated). A similar optimization in React is to use the Memo wrapped component, which can be used in both classes and functions, as well as custom comparison logic. These methods are used by the parent component to prevent the parent component from performing invalid updates. This is done by caching props, using useMemo and useCallback to wrap props to the child component.

2. React can affect performance

  1. Using the context incorrectly can cause all child components to re-render, whether they are consumers of the context or not

    ❌ Uses the anti-pattern of context: Updates to context subcomponents and context value are placed in one component, resulting in all consuming and non-consuming components being updated equally when the context value is updated

    Codesandbox. IO/embed/jolly…

    ✅ Use context correctly: Separate the management of the context value from the consuming component. When the context value is updated, only the consuming component is updated, not the non-consuming component

    Codesandbox. IO/embed/aged -…

    In addition to separating the context value management from the context consumption component, we can also package the consumption component with reac.Memo () to avoid invalid updates to the consumption component.

  2. Because the React component defaults to policies that update the props based on shallow comparisons, referencing types of props causes child components to make unnecessary updates along with parent components

    In class components, to prevent this, avoid creating props for referencing types in the Render method and passing them to the child component. Otherwise, every time the parent component rerenders, the props for referencing types will be a new value, causing the child component to rerender. Avoid creating new references in render. For example, don’t pass inline functions in props. Callbacks should be initialized on properties of class components, and use arrow function syntax (bind this, see article).

    In a function component, you can wrap props with useMemo and useCallback and pass them to child components. These hooks remember the values that were calculated, so that each component can use the result of the previous calculation, and if the result is an object, the reference to it will not change. So it does not cause child components to rerender.

  3. Multiple calls outside of the React component lifecycle method and React event callbacksetState, resulting insetStateCannot be batch, resulting in multiple re-renders

    We know that setState may be asynchronous, which does not mean that setState is performed asynchronously, but that the effect of setState occurs asynchronously, that is, the update of state is asynchronous. React setstates are merged into a single update for the same batch of transactions, which improves React performance. SetState will be batched once if:

    • Multiple setStates are executed in the same React composite event callback
    • Multiple setStates are executed in the same React lifecycle
    export default function App() {
      const [a, setA] = useState(0);
      const [b, setB] = useState(0);
      const [c, setC] = useState(0);
      const handleClick = () = > {
        setA((a) = > a + 1);
        setB((b) = > b + 2);
        setC((c) = > c + 3);
      };
      console.log("render");
      return (
        <div className="App">
          <h1>Hello CodeSandbox</h1>
          <button onClick={handleClick}>click</button>
          <h2>
            {a}-{b}-{c}
          </h2>
        </div>
      );
    }
    Copy the code

    If setState is not called in the React composite event, such as when setState is called multiple times, the update of the state is synchronous and cannot be quantified. Component updates with each setState:

    • DOM native event callback
    • In asynchronous callbacks, promise.then(), setTimeout
    export default function App() {
      const [a, setA] = useState(0);
      const [b, setB] = useState(0);
      const [c, setC] = useState(0);
      const handleClick = () = > {
        setTimeout(() = > {
          setA((a) = > a + 1);
          setB((b) = > b + 2);
          setC((c) = > c + 3);
        }, 0);
    	/* Promise.resolve().then(() => { setA((a) => a + 1); setB((b) => b + 2); setC((c) => c + 3); }); * /
      };
      console.log("render");
      return (
        <div className="App">
          <h1>Hello CodeSandbox</h1>
          <button onClick={handleClick}>click</button>
          <h2>
            {a}-{b}-{c}
          </h2>
        </div>
      );
    }
    Copy the code

    In this case, we can change the structure of the state, combine multiple states once, and minimize the number of setStates

    After React 18, all setstates will be batch by default, both in React synthetic event callbacks and in asynchronous callbacks such as setTimeout. FluchSyc (() {setState… }) can skip batch processing

3. Immutable data

Immutable data is when you try to change the value of a variable, it creates a new variable to replace the original variable, rather than directly modifying the original variable, which is immutable.

Immutable data is part of functional programming

This is especially important for React. React doesn’t allow us to directly modify state for a reason, but this is also due to its design. Instead of directly modifying state, React can compare the old and new states to determine whether to update components. If state is a deeply nested object, it can only be compared deeply recursively. This is a drain on application performance and an inconvenience to developers. If only shallow comparisons are allowed (with ===,! The == operator) tells you whether the component should be updated more efficiently. So when we update the state in the React component, we’ll say:


state = {
    user: {
      name:  'chege'.age: 24.friend: {
        name: 'xiaoming'.age: 22}}}addAge() {
  // spread operator ... 
	this.setState({
    user: {
      ...this.state.user,
      age: 25}})/* Or Object.assign this.setState(Object.assign({}, this.state.user, {age: 25})) */
}

Copy the code

. Both object.assign () and object.assign () will shallow copy objects. New and old objects will only be referenced differently in the first layer. This ensures efficient updates for components that rely on shallow objects, while avoiding unnecessary updates for components that rely on deep objects, because we only change the shallow objects, not the deep objects.

In Redux, reducer is also a pure function and state cannot be modified, for the same reason as React, to make it easier to determine whether state is updated:

Do not modify state. Create a new copy using object.assign (). You cannot use object.assign (state, {visibilityFilter: action.filter}) as this will change the value of the first argument. You must set the first parameter to empty object. You can also turn on support for the ES7 proposal object expansion operator to use {… state, … NewState} serves the same purpose.

But if the state object were a little deeper, we might have to write a bunch of… And object.assign (), which results in code that looks bad:

state = {
    user: {
    name:  'chege'.age: 24.friend: {
      name: 'xiaoming'.age: 22}}}addFriendAge() {
  // spread operator ... 
	this.setState({
          user: {
	    ...this.state.user,
           friend: {
             ...this.state.user.friend,
             age: 23}}})/* Or Object.assign this.setState(Object.assign({}, Object.assign({}, this.state.user,), {age: 25})) */
}

Copy the code

If you want to balance code aesthetics with application performance, you can use third-party libraries that create persistent data structures that reuse parts of old objects that do not need to be changed when creating immutable objects, saving time and space required to create objects.

Look at the shallow copy above… The React and Object.assign() methods were optimized, but they weren’t optimal. In addition to ugly code, shallow copies still cost a bit of performance. When the copied Object contained 100,000 properties, JavaScript has to make an honest copy of every property, or if the property is of a primitive type, a copy of the primitive value. So shallow copy still wastes memory and program running time. One implementation of immutable data that can be used for more than React optimization is Persistent data structures, which aim to create immutable data that returns a new version with each change, without directly modifying the original data. At the same time, to save memory and modification time, reuse the parts that have not changed, this is called structural sharing.

There are many ways to implement persistent data structures and structure sharing. We will not expand them here, but only discuss the methods used by several js libraries that implement immutable data:

  • Immutable. Js: Its persistent data structure is based on a prefix tree (TRIe, also known as dictionary tree), in which 0 and 1 represent left and right nodes, path from top to bottom nodes represents keys, and leaf nodes represent key values. Immutable properties are realized by copying paths. For example, if you want to change a leaf node (path 101, value 12), you just copy the nodes on that path and share the nodes next to it. Each modification will produce a new path like the one on the right, but the structure of the tree does not change. The new path and the original unchanged node can also form a new tree, and the structure is the same, realizing the sharing of the unmodified part. That’s the general idea. In more detail, keys are hashed to numeric values, which are then converted to binary, and mapped to key values by bitpartitioning as keys. For more details, see here:

    Immutable. Js, persistent data structures and structure sharing – why use Immutable. Js instead of plain JavaScript objects?

    How does Immutable. Js persist data structures?

  • Immer. js: it does not implement its own data persistence structure, but only hijacks data modification through proxy, internally maintains two copies of data before and after modification, and uses the data structure as the native object of JS, which will consume very much performance when the amount of data is large. The advantage of immer.js is that it has few apis and is easy to use.

Using these libraries, we can modify the data directly and return it as a new data without modifying the original data, and the previous and previous data will only be disreferenced in the modified part, and the unmodified part will share the reference.

Use immer.js to change state.

import produce from "immer"

state = {
    user: {
	name:  'chege'.age: 24.friend: {
	 name: 'xiaoming'.age: 22}}}addFriendAge() {
  this.setState(produce(draft= > {
     draft.user.firend.age= 23}}))Copy the code

We can test whether immer.js can share unchanged parts:

import produce from "immer";

const state = {
  user: {
    name: "chege".age: 24.friend1: {
      name: "xiaoming".age: 22
    },
    friend2: {
      name: "xiaoming".age: 22}}};const nextState = produce(state, (draftState) = > {
  draftState.user.friend1.age = 23;
});

console.log(nextState.user === state.user); // false, user belongs to a node of the changed path
console.log(nextState.user.friend1 === state.user.friend1); // false, friend1 belongs to a node of the changed path
console.log(nextState.user.friend2 === state.user.friend2); 
// true, friend2 is not a node of the changed path and should be shared
Copy the code

4. Reduce double counting

The states in Redux are placed in a tree, and when a component subscribes to a state, it needs to deconstruct the store from the top, working its way up and down to the target state, possibly with some computation to generate the derived data that will eventually be realized on the UI. When the store is updated, all components subscribed to state will receive the update notification and recalculate their respective subscription data. Even if the subscribed part is not updated, or the final calculation is the same at that time, the recalculation will be done:

In the example below, when we click Add a, both ConnectChildA and ConnectChildB’s mapStateToProps will execute, even though the state of the ConnectChildB subscription is not updated. But they all depend on a store, so they all receive updates, and all store-dependent components are fully updated.

Codesandbox. IO/embed/confi…

Because of redux’s design, any state in the Store will fully update all of the following components, which is unavoidable, but avoids too much invalidation during updates. The results are cached at the mapStateToProps node to avoid unnecessary component updates. See reselector for more details:

GitHub – reduxjs/reselect: Selector library for Redux