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:
-
Click the Profiler record button in the React Developer Tools plugin to start recording. The button will turn red
-
Enter a character in the input box
-
Click the Add button to add your input to TodoList
- Click the record button again to end the recording
Start looking at the data
Meaning of each part:
- The current TAB is in flame mode;
1/2
Represents, 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;- Gray represents components that are not rendered, such as
App
,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; - 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
TodoInput
Trigger rendering
- In the second rendering
TodoList
Trigger renderingTodoInput
Trigger renderingTodoItem
Trigger 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:
- The first rendering is done by entering a character in the input field
a
.TodoInput
In thestate
Change, trigger rerender, there’s nothing wrong with that. - The second rendering is because of the click
add
Button to add data toTodoList
, because a new data, soTodoList
Trigger rerender,TodoItem
It also triggers re-rendering, there’s nothing wrong with that, but the weird thing is, whyTodoInput
Also 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
useCallback
Is 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 changes
useCallback
Will 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
memo
anduseCallback
It needs to be used at the same time or it won’t work (I’ve actually seen just writeuseCallback
The code -_ – | |);memo
For child components,useCallbak
For 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.
usecreateSlice
To 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:
- Why is there an extra render?
- Why is it
useReducer
Changing 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
- React has two common tools
React Developer Tools
andwhy-did-you-render
, optimization can start by reducing the number of renders and rendering times; React.memo
anduseCallback
oruseMemo
At the same time, it can reduce the unnecessary child component rendering when the parent component rendering;react-redux
theuseSeletor
There 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…