takeaway

This article is for people who want to optimize the React project but don’t know how to do it. This paper introduces the common debugging tools and optimization means and the whole optimization thinking process.

background

I have recently encountered some React performance issues at work.

The request clicks on an Add item button to add the item to the cart, and the number of items is delayed for nearly 1s before changing. After a series of optimizations, the render count was optimized from 20 to 4 and the render time was reduced to milliseconds.

In the meantime, I learned some debugging and optimization tips for React function components (I haven’t used class components since I used hook, really delicious 😄). Therefore, I want to write a hydrological record and share the psychological process of this optimization.

The body of the

Here we use the TodoList example 🌰 as the basis, and then step by step through the debugging tool, find the points that can be optimized, and then step by step optimization.

First, useCreate-React-AppCreate a simple project:

npx create-react-app react-optimize-practice --template typescript
Copy the code

And then, let me write a simple oneTodoList, the hierarchical structure is as follows:

The code is as follows:

TodoList.tsx

import { FC } from "react";
import { useState } from "react";
import TodoInput from "./TodoInput";
import TodoItem from "./TodoItem";

type TodoItemType = {
  id: number;
  text: string;
  isComplete: boolean;
};

const TodoList: FC = () = > {
  const [todoList, setTodoList] = useState<TodoItemType[]>([]);

  const handleAddItem = (text: string) = > {
    if(! text)return;
    setTodoList((preTodoList) = > {
      return [
        ...preTodoList,
        { text, isComplete: false.id: +new Date()},]; }); };return (
    <div>
      <TodoInput onAddItem={handleAddItem} />
      <ul>
        {todoList.map((item) => {
          return (
            <TodoItem
              key={item.id}
              text={item.text}
              isComplete={item.isComplete}
            />
          );
        })}
      </ul>
    </div>
  );
};

export default TodoList;
Copy the code

TodoInput.tsx

import { FC, useState } from "react";

type Props = {
  onAddItem: (text: string) = > void;
};

const TodoInput: FC<Props> = ({ onAddItem }) = > {
  const [text, setText] = useState(' ');

  const handleAdd = () = > {
    onAddItem(text);
  };

  return (
    <div>
      <input value={text} onChange={(e)= > setText(e.target.value)} />
      <button onClick={handleAdd}>add</button>
    </div>
  );
};

export default TodoInput;
Copy the code

TodoItem.tsx

import { FC } from "react";

type Props = {
  text: string;
  isComplete: boolean;
};

const TodoItem: FC<Props> = ({ text, isComplete }) = > {
  return (
    <li>
      <button>x</button>
      <input type="checkbox" checked={isComplete} />
      <span>{text}</span>
    </li>
  );
};

export default TodoItem;
Copy the code

Effect of the page

The complete code

React Developer Tools

The installation

React Developer Tools is a Chrome plugin that allows you to debug React projects

Need over the wall, if can’t double wall, with a new version of the Edge line also: [Microsoft Edge add-in – react developer tools] (microsoftedge.microsoft.com/addons/sear… developer tools? hl=zh-CN)

use

Now we use React Developer Tools to check the render times and counts.

Perform the following operations:

  1. Click the Profiler record button in the React Developer Tools plugin to start recording. The button will turn red

  2. Enter a character in the input box

  3. Click the Add button to add your input to TodoList

  1. Click the record button again to end the recording

Start looking at the data

Meaning of each part:

  1. The current TAB is in flame mode;
  2. 1/2Represents, a total of 2 renderings were triggered during recording, and the data currently viewed is the first rendering; There are also two pillars, like the number of renders, blue is the currently selected render, and the height of the pillars represents the render time;
  3. Gray represents components that are not rendered, such asApp,TodoList; Colors represent components that are being rendered, green means fast rendering time, or yellow or red if rendering is slow (you can focus at this point); You can click the corresponding component to view the detailed rendering data;
  4. Detailed rendering data for the currently selected component. Click the component in box “3” to switch components.

Take a look at the contents of another TAB

Everything else is the same here, except that the component rendering flame below is now sorted by component rendering time (sorted by rendering time in reverse order). Components that are not rendered are not shown. This component ranking chart is a good way to find the components that took the longest to render.

Troubleshoot problems

Click to see information for the second rendering

From the first rendering and second rendering above, the following information can be obtained:

  • In the first render
    • TodoInputTrigger rendering
  • In the second rendering
    • TodoListTrigger rendering
    • TodoInputTrigger rendering
    • TodoItemTrigger rendering

To optimize the

Optimization can be done from two perspectives

  • One is to reduce the number of renders
  • Second, reduce the total render time per render (reduce component render time)

There are two operations here, so the number of renders is two. There is no room for optimization from the point of view of the number of renders, so the main concern here is how to reduce the total rendering time

Analysis:

  1. The first rendering is done by entering a character in the input fielda.TodoInputIn thestateChange, trigger rerender, there’s nothing wrong with that.
  2. The second rendering is because of the clickaddButton to add data toTodoList, because a new data, soTodoListTrigger rerender,TodoItemIt also triggers re-rendering, there’s nothing wrong with that, but the weird thing is, whyTodoInputAlso trigger rerender?

This raises the question: What triggers the React component to rerender?

A: Change of state or change of props

TodoInput triggers rerendering in either of these cases. On the second rendering, the state obviously doesn’t change, so the only thing that changes is props. The parent component TodoList passes in just one props: onAddItem

In fact, every time a function component is re-rendered, it is called again. So when TodoList rerenders, the handleAddItem is regenerated, which is equivalent to passing a new onAdd method to the TodoInput component. Is there a way to cache the handleAddItem so that the props passed to TodoInput are the same every time? This eliminates the need to render TodoInput, reducing the total time for the second rendering.

React provides a hook called useCallback to cache functions.

useCallback

Want to see the official documentation of useCallback explain poke me

usage

  • useCallbackIs a function that returns a new function;
  • The first argument is passed to the function you want to cache;
  • The second argument is an array that represents the dependency array when the dependency array changesuseCallbackWill return a new function, if there are no dependencies, write an empty array.

Let’s modify the original code

// TodoList.tsx
import { useCallback } from "react";

const handleAddItem = useCallback((text: string) = > {
  if(! text)return;
  setTodoList((preTodoList) = > {
    return [...preTodoList, { text, isComplete: false.id: +new Date()}]; }); } []);Copy the code

Now test it again and you’ll see that nothing has changed. TodoInput or render

According to???

The reason for this is that while the reference to the handleAddItem is kept constant through useCallback, the Memo function provided by React is used instead of the TodoInput being rerendered

The complete code

React.memo

I want to see the react. memo official document

Compare the props of the function component. If the props is unchanged, do not render.

usage

  • The Memo is a higher-order component that returns a new component. The memo is the PureComponent of the function component.

  • The first parameter is the component to be wrapped;

  • The second argument is a comparison function that does not pass the default shallow comparison to props.

Change the original code

import { FC, memo } from "react";

type Props = {
  onAddItem: (text: string) = > void;
};

const TodoInput: FC<Props> = ({ onAddItem }) = > {
  / /... slightly
};

export default memo(TodoInput);
Copy the code

Test results:

At this point, we’ll see that TodoInput is grayed out, and there’s a Memo tag for TodoInput because the Memo comparison doesn’t trigger rendering.

This is how TodoList rendering time is optimized, with two points to note

  1. memoanduseCallbackIt needs to be used at the same time or it won’t work (I’ve actually seen just writeuseCallbackThe code -_ – | |);
  2. memoFor child components,useCallbakFor the parent component, don’t get confused.

React also provides useMemo to cache other types of data. UseCallback can be used in the same way as useCallback, but it can return any value. UseCallback can only return functions and is a subset of useMemo.

The complete code

why-did-you-render

Here is another useful debugging tool: github.com/welldone-so…

This library can print out the reason for each action component to be re-rendered, so it is good for finding the reason for rendering in many components that hook uses.

The installation

yarn add --dev @welldone-software/why-did-you-render
Copy the code

or

npm install @welldone-software/why-did-you-render --save-dev
Copy the code

Create a new file/SRC /wdyr.ts

/// <reference types="@welldone-software/why-did-you-render" />
import React from "react";

// Do not open it in a build environment, which affects performance
if (process.env.NODE_ENV === "development") {
  const whyDidYouRender = require("@welldone-software/why-did-you-render");
  whyDidYouRender(React, {
    trackAllPureComponents: true.// Trace all pure components (react.pureComponent or react.memo)
  });
}
Copy the code

Then import in index.tsx

import './wdyr'; // <-- import in the first line
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>.document.getElementById('root'));Copy the code

debugging

Perform the same steps, type a character and click the Add button to open the console and find an error

This is a React error. The React component does not add the onChange event. TodoList passes the checkbox change function to the TodoItem.

// TodoItem.tsx
import { ChangeEventHandler, FC } from "react";

type Props = {
  text: string;
  isComplete: boolean;
  onCheckboxChange: (checked: boolean) = > void;
};

const TodoItem: FC<Props> = ({ text, isComplete, onCheckboxChange }) = > {
  const handleCheckboxChange: ChangeEventHandler<HTMLInputElement> = (e) = >
    onCheckboxChange(e.target.checked);

  return (
    <li style={{ display: "flex", flexDirection: "row", alignItems: "center", paddingBottom: 5}} >
      <input
        type="checkbox"
        checked={isComplete}
        onChange={handleCheckboxChange}
      />
      <span style={{marginLeft: 5}} >{text}</span>
      <button style={{marginLeft: 5}} >x</button>
    </li>
  );
};

export default TodoItem;
Copy the code
// TodoList.tsx
import { FC, useCallback } from "react";
import { useState } from "react";
import TodoInput from "./TodoInput";
import TodoItem from "./TodoItem";

type TodoItemType = {
  id: number;
  text: string;
  isComplete: boolean;
};

const TodoList: FC = () = > {
  const [todoList, setTodoList] = useState<TodoItemType[]>([]);

  const handleAddItem = useCallback((text: string) = > {
    if(! text)return;
    setTodoList((preTodoList) = > [
      ...preTodoList,
      { text, isComplete: false.id: +new Date()}); } []);const handleChangeBox = (id: number) = > (checked: boolean) = > {
    setTodoList((preTodoList) = >
      preTodoList.map((item) = >
        item.id === id ? { ...item, isComplete: checked } : item
      )
    );
  };

  return (
    <div>
      <TodoInput onAddItem={handleAddItem} />
      <ul style={{ margin: 10.padding: 0}} >
        {todoList.map((item) => {
          const { id, text, isComplete } = item;
          return (
            <TodoItem
              key={id}
              text={text}
              isComplete={isComplete}
              onCheckboxChange={handleChangeBox(id)}
            />
          );
        })}
      </ul>
    </div>
  );
};

export default TodoList;
Copy the code

Create React App (CRA) ^4: Create React App (CRA) ^4: Create React App (CRA) ^4: Create React App (CRA) ^4 If there is any big boss know what problem, please inform little brother.

Now add a wDYR configuration to each component, which means to listen for the cause of the print trigger.

// TodoInput.tsx
import { ChangeEventHandler, FC, memo, useState } from "react";

type Props = {
  onAddItem: (text: string) = > void;
};

const TodoInput: FC<Props> = ({ onAddItem }) = > {
  / /... slightly
};

TodoInput.whyDidYouRender = {
  logOnDifferentValues: true,}export default memo(TodoInput);
Copy the code

The same goes for other component configurations, so I won’t post the code here.

Then do it again, and you can see the output from the console

Here you can clearly see what components trigger rerendering for what reason.

The complete code

Function components use react-redux pits

In order to solve this problem, sorry, in order to reproduce this problem, let’s change the code.

The introduction ofreact-reduxSuch as the library

yarn add react-redux
yarn add -D @types/react-redux
yarn add @reduxjs/toolkit
Copy the code

@reduxjs/ Toolkit is a library that writes like DVA, but has better Typescript support than DVA, which has not been maintained for a long time. The following code is written using @reduxjs/ Toolkit.

usecreateSliceTo create atodoList.ts

// src/model/todoList.ts
import { createSlice, PayloadAction } from "@reduxjs/toolkit";

export type TodoItemType = {
  id: number;
  text: string;
  isComplete: boolean;
};

interface TodoListState {
  data: TodoItemType[];
}

const initialState: TodoListState = {
  data: [],};// createSlice is the dVA equivalent of creating a model
export const todoListSlice = createSlice({
  name: "todoList".// Namespace
  initialState, / / initial value
  reducers: {
    // Add an item to todoList
    add: (state, action: PayloadAction<string>) = > {
      // State can be changed directly because @reduxjs/ Toolkit uses the Immer library
      state.data.push({
        id: +new Date(),
        isComplete: false.text: action.payload,
      });
    },
    // Delete a todoList item
    remove: (state, action: PayloadAction<number>) = > {
      state.data.filter((item) = >item.id ! == action.payload); },// Update a todoList post
    update: (state, action: PayloadAction<TodoItemType>) = > {
      state.data = state.data.map((item) = >item.id === action.payload.id ? action.payload : item ); ,}}});/ / export actions
export const { add, remove, update } = todoListSlice.actions;

export default todoListSlice.reducer;
Copy the code

Create a warehouse

// src/model/index.ts
import { configureStore } from "@reduxjs/toolkit";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import todoListReducer from "./todoList";

const store = configureStore({
  reducer: {
    todoList: todoListReducer,
  },
});

// Infer the RootState type from the store itself
type RootState = ReturnType<typeof store.getState>;
{todoList: TodoListState}}
type AppDispatch = typeof store.dispatch;

// Use the added types useDispatch and useSelector in app
export const useAppDispatch = () = > useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

export default store;
Copy the code

Connect the repository to React

import "./wdyr";
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import store from "./model";
import { Provider } from "react-redux";

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>.document.getElementById("root"));Copy the code

Use, modifyTodoList.tsx

import { FC, useCallback } from "react";
import { useAppDispatch, useAppSelector } from ".. /model";
import { add, TodoItemType, update } from ".. /model/todoList";
import TodoInput from "./TodoInput";
import TodoItem from "./TodoItem";

const TodoList: FC = () = > {
  const todoList = useAppSelector(({ todoList }) = > todoList.data);
  const dispatch = useAppDispatch();

  const handleAddItem = useCallback(
    (text: string) = > {
      if(! text)return;
      dispatch(add(text));
    },
    [dispatch]
  );

  const handleChangeBox = (item: TodoItemType) = > (checked: boolean) = >{ dispatch(update({ ... item,isComplete: checked }));
  };

  return (
    <div>
      <TodoInput onAddItem={handleAddItem} />
      <ul style={{ margin: 10.padding: 0}} >
        {todoList.map((item) => {
          const { id, text, isComplete } = item;
          return (
            <TodoItem
              key={id}
              text={text}
              isComplete={isComplete}
              onCheckboxChange={handleChangeBox(item)}
            />
          );
        })}
      </ul>
    </div>
  );
};

TodoList.whyDidYouRender = {
  logOnDifferentValues: true};export default TodoList;
Copy the code

Dispatch can be used in two ways

The first kind of

Use the exported action directly

dispatch(add(text)); 
Copy the code

The second,

As with DVA, use the string “namespace /action name”,

dispatch({
  type: 'todoList/add'.payload: text
}); 
Copy the code

I prefer the first one because the code hints are more fragrant ☺️

So far there’s nothing wrong with this, the console doesn’t print anything.

But if you write use Selector a different way, it’s different

import { FC, useCallback } from "react"; import { useAppDispatch, useAppSelector } from ".. /model"; import { add, TodoItemType, update } from ".. /model/todoList"; import TodoInput from "./TodoInput"; import TodoItem from "./TodoItem"; const TodoList: FC = () => {- const todoList = useAppSelector(({ todoList }) => todoList.data);
+ const { todoList } = useAppSelector(({ todoList }) => ({
+ todoList: todoList.data,
+}));/ /... Slightly}; TodoList.whyDidYouRender = { logOnDifferentValues: true, }; export default TodoList;Copy the code

Here we return an object with useAppSelector, which is actually common when you want to get multiple values from different reducer at once.

Now let’s refresh the page and look at the console.

Nothing is done, initialization is an additional useReducer refresh.

Troubleshoot problems

Two questions arise:

  1. Why is there an extra render?
  2. Why is ituseReducerChanging the trigger rendering is not used in the codeuseReducer?

To solve the second problem, actually react-redux uses the useReducer to forcibly refresh the store data when listening to changes. UseSelector source code can be clearly seen in the following figure, which is also recommended by the official website.

Let’s take a look at the first problem, where you need to modify the configuration of wdyr.ts to see the log of useSelector triggering updates

// src/wdyr.ts /// <reference types="@welldone-software/why-did-you-render" /> import React from "react"; // Do not open it in a build environment, it affects performance if (process.env.node_env=== "development") {
  const whyDidYouRender = require("@welldone-software/why-did-you-render");
+ const ReactRedux = require("react-redux");WhyDidYouRender (React, {trackAllPureComponents: true, // Trace all pure components (react.pureComponent or react.memo)+ trackExtraHooks: [[ReactRedux, "useSelector"]], // Track useSelector
  });
}
Copy the code

Now looking at the console, it looks like the data hasn’t changed, but two renders have been triggered

This is because useSelector returns data using the === comparison. If you return an object, each comparison will be false, so multiple renders will be triggered

To optimize the

There are several solutions to the first problem.

The first kind of

Instead of returning an object, write it the way you started, using multiple usesElectors if you have more than one value to return.

  const todoList = useAppSelector(({ todoList }) = > todoList.data);
Copy the code

The second,

If you must write an object, you can use the second argument to useSelector, passing in a comparison function.

Here on the official website for us to export a comparison function, can be used directly.

import { shallowEqual } from "react-redux";

const { todoList } = useAppSelector(
  ({ todoList }) = > ({
    todoList: todoList.data, }), shallowEqual ); i.Copy the code

Re-open the console to see the effect without any extra re-rendering.

You can also use third-party library comparison functions such as react-fast-compare, Lodash /isEquald, etc.

The third kind of

Use resELECT to cache useSelector.

A fourth

Work with useMemo.

import { useMemo } from 'react';
import { useSelector } from 'react-redux';

/** **/
const { todoList, usename } = useAppSelector(({ todoList, user }) = > ({
  todoList: todoList.data,
  usename: user.username,
}));

/** **/
const state = useAppSelector((state) = > state); // Leave the value returned by useSelector unchanged

// Use useMemo to split data
const { todoList, usename } = useMemo(() = > {
  return {
    todoList: state.todoList.data,
  	usename: state.user.username
  };
}, [state])
Copy the code

The complete code

conclusion

  1. React has two common toolsReact Developer Toolsandwhy-did-you-render, optimization can start by reducing the number of renders and rendering times;
  2. React.memoanduseCallbackoruseMemoAt the same time, it can reduce the unnecessary child component rendering when the parent component rendering;
  3. react-reduxtheuseSeletorThere are four ways to solve this problem.

In fact, these performance optimization points, are in the beginning of the code can be avoided, write more than two times will be familiar. Many people find React expensive to learn, probably because they are not familiar with the render mechanism of React. In VUE, these optimization frameworks are already in place, but React needs to be written itself.

Reference:

  • Overreacted. IO/useful – Hans/a – c…
  • React-redux.js.org/api/hooks#p…
  • Zh-hans.reactjs.org/docs/hooks-…
  • Github.com/welldone-so…