Article first personal blog
preface
purpose
This article covers only the performance optimizations that are specific to functional components, not those that exist for both class and functional components, such as the use of keys. In addition, this article will not go into the API usage in detail, but it is difficult to use good hooks.
For the reader
Experience with React functional components, hooks, useState, useCallback, useMemo apis and at least some documentation. If you have experience with performance optimization of class components, this article will feel familiar.
React performance optimization
I think the React performance optimization concept has two main directions:
-
Reduce the number of rerender times. Because the heaviest piece (which takes the longest time) in React is reconciliation(which can be simply interpreted as diff). If you do not render, it will not be reconciliation.
-
Reduce the amount of computation. For functional components, Render executes the function call from scratch every time.
When using class components, the React optimization API is mainly: ShouldComponentUpdate/PureComponent/shouldComponentUpdate/PureComponent/shouldComponentUpdate/PureComponent/shouldComponentUpdate/PureComponent Although it is also possible to prevent the current component from rendering when state is updated, if you want to do this, prove that you should not use this property as a state property, but rather as a static property or as a simple variable outside the class.
But there are no declared cycles and no classes in functional components, so how do you optimize performance?
React.memo
The first is the React.Memo API, which is a PureComponent of the standard class component, which can reduce the number of rerender times.
Examples of possible performance problems
Take 🌰 as an example. First, let’s look at two pieces of code:
In the root directory there is an index.js with the following code, which implements something like: a title at the top, a button in the middle (click on the button to change the title), and a puppet component at the bottom, passing in a name.
// index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import Child from './child'
function App() {
const [title, setTitle] = useState("This is a title.")
return (
<div className="App">
<h1>{ title }</h1>
<button onClick={()= >SetTitle ("title has changed ")}> Change the name</button>
<Child name="Peach peach"></Child>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Copy the code
There is one child-.js in the sibling directory
// child.js
import React from "react";
function Child(props) {
console.log(props.name)
return <h1>{props.name}</h1>
}
export default Child
Copy the code
When first rendered it looks like this:
The console will print “peaches” to prove that the Child component is rendered.
Next click on the Change name button and the page will change to:
The title has changed, and the console prints “peaches”, so you can see that even though we changed the state of the parent, the parent is rerendered, and the child is rerendered. You might be thinking that the props passed to the Child component are the same. If only the Child component didn’t rerender it, why would you think that?
We assume that the Child component is a very large component, and rendering it at once will consume a lot of performance. We should try to minimize the renderings of the component, otherwise it will cause performance problems. So if the Child component does not change, it should not render even if the parent rerendered it.
So how can we make sure that the child component does not render when the props are unchanged?
The answer is to use react. memo to render the same props for the same props, and to improve the performance of the component by memorizing the component’s rendering results.
The basic use of React. Memo
Memo is actually a higher-order function that passes a component into it and returns a memorizable component.
function Component(props) {
/* Render with props */
}
const MyComponent = React.memo(Component);
Copy the code
The Child component in the example above can be changed to look like this:
import React from "react";
function Child(props) {
console.log(props.name)
return <h1>{props.name}</h1>
}
export default React.memo(Child)
Copy the code
The component wrapped via React. Memo will not be rerendered as long as the props are the same. In the example above, after I click change name, only the title will change. However, the Child component will not be rerendered (the effect is that the log in the Child will not be printed in the console), and will directly reuse the results of the last rendering.
This effect is very similar to the PureComponent effect in class components, except that the former is used for function components and the latter for class components.
React. Memo Advanced usage
By default, it only compares complex props (props). If you want to control the comparison, pass in a custom comparison function as a second argument.
function MyComponent(props) {
/* Render with props */
}
function areEqual(prevProps, nextProps) {
Return true if passing nextProps to render returns the same result as passing prevProps to render, false otherwise */
}
export default React.memo(MyComponent, areEqual);
Copy the code
This section is from the React website.
If you’ve ever used the shouldComponentUpdate() method in a class component, you’ll be familiar with the react.memo second argument, but it’s worth noting that areEqual returns true if props areEqual; If props are not equal, return false. This is the opposite of the value returned by the shouldComponentUpdate method.
useCallback
Add a subtitle to the requirement and a button to change the subtitle. Then put the button to change the title in the Child component.
The purpose of putting the titling-changing button in the Child component is to pass the titling-changing event to the Child component via props and see if it causes performance problems.
First look at the code:
The parent component index. Js
// index.js
import React, { useState } from "react";
import ReactDOM from "react-dom";
import Child from "./child";
function App() {
const [title, setTitle] = useState("This is a title.");
const [subtitle, setSubtitle] = useState("I am a subtitle");
const callback = (a)= > {
setTitle("The title has changed.");
};
return (
<div className="App">
<h1>{title}</h1>
<h2>{subtitle}</h2>
<button onClick={()= >SetSubtitle (" subtitle changed ")}> Change subtitle</button>
<Child onClick={callback} name="Peach peach" />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Copy the code
Subcomponents child. Js
import React from "react";
function Child(props) {
console.log(props);
return (
<>
<button onClick={props.onClick}>Change the title</button>
<h1>{props.name}</h1>
</>
);
}
export default React.memo(Child);
Copy the code
The first rendering
When first rendered, this code will show what the image above looks like, and the console will print out the peaches.
Then when I click on this button to change the subtitles, subtitle becomes “subtitle change”, and the console will print out the peach peach again, it is proved that the subcomponents again apply colours to a drawing, but the Child components don’t have any change, then the Child component to apply colours to a drawing is redundant, so how to avoid off the redundant rendering?
Looking for a reason
Before we solve the problem, we should first know what causes the problem.
Let’s analyze, a component is rerendered, generally three cases:
-
Or the state of the component itself changes
-
Either the parent component is rerendered, causing the child component to be rerendered, but the parent component’s props are not modified
-
Either the parent component is rerendering, causing the child component to be rerendered, but the props passed by the parent is changed
Next, use the method of elimination to find out what causes it:
The first rule obviously rules out that clicking change subtitle does not change the state of the Child component.
The parent component is rerendered. The props passed by the parent component to the child component are the same, but the child component is rerendered. We used react. memo to solve this problem, so this case is excluded.
So, in case 3, when the parent component rerenders, the props passed to the Child component changes, and then the two properties passed to the Child component are name and onClick. Name is the constant passed to the Child component, so it doesn’t change, so it changes onClick. Why did the callback function passed to onClick change? As mentioned at the beginning of this article, each time a functional component is rerendered, the function component is reexecuted from the beginning, so the callback function created in both instances must have changed, causing the child component to be rerendered.
How to solve
The solution is to use the useCallback API to keep the references to the two functions consistent when rerendering without any changes.
UseCallback Usage
const callback = (a)= > {
doSomething(a, b);
}
const memoizedCallback = useCallback(callback, [a, b])
Copy the code
Passing the function and its dependencies as arguments to useCallback, it returns a memoized version of the callback function that will only be updated if the dependencies change.
We can change index.js to look like this:
// index.js
import React, { useState, useCallback } from "react";
import ReactDOM from "react-dom";
import Child from "./child";
function App() {
const [title, setTitle] = useState("This is a title.");
const [subtitle, setSubtitle] = useState("I am a subtitle");
const callback = (a)= > {
setTitle("The title has changed.");
};
// Pass the memory callback to the Child via useCallback
const memoizedCallback = useCallback(callback, [])
return (
<div className="App">
<h1>{title}</h1>
<h2>{subtitle}</h2>
<button onClick={()= >SetSubtitle (" subtitle changed ")}> Change subtitle</button>
<Child onClick={memoizedCallback} name="Peach peach" />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Copy the code
This way we can see that only the peaches are printed on the first render, and the peaches are not printed when clicking change subtitle and change title.
If we pass arguments to the callback that need to be added to the cache when the arguments change, we can put the arguments in the second argument array of useCallback as a form of dependency, similar to useEffect.
useMemo
As mentioned at the beginning of this article, React performance is optimized in two ways: one is to reduce the number of rerender times (or unnecessary rendering), and the other is to reduce the amount of computation.
The react. memo and useCallback introduced earlier are designed to reduce the number of rerender times. And to reduce the amount of computation, that’s what useMemo does, so let’s look at an example.
function App() {
const [num, setNum] = useState(0);
// A very time-consuming calculation function
// result returns a value of 49995000
function expensiveFn() {
let result = 0;
for (let i = 0; i < 10000; i++) {
result += i;
}
console.log(result) / / 49995000
return result;
}
const base = expensiveFn();
return (
<div className="App">
<h1>Count: {num}</h1>
<button onClick={()= > setNum(num + base)}>+1</button>
</div>
);
}
Copy the code
The first rendering looks like this:
For this example, click the +1 button and add the current value (num) to the value after the call to expensiveFn. Then set and to num and display it. In the console, it will print 49995000.
Performance issues may arise
Even a seemingly simple component can cause performance problems, so let’s take a look at the simplest example to see what else is worth optimizing for.
First we treat the expensiveFn function as a calculated function (for example you can replace “I” with “10000000”), and then every time we click the +1 button, the component will be rerendered and the expensiveFn function will be called and output 49995000. Since each call to expensiveFn returns the same value, we can find a way to cache the calculated value and return the cached value directly each call to the function so we can do some performance tuning.
UseMemo does the result caching
For the above problem, use useMemo to cache the value of the expensiveFn function after execution.
First introduce the basic use method of useMemo, detailed use method can be seen on the official website:
function computeExpensiveValue() {
// Computation-based code
return xxx
}
const memoizedValue = useMemo(computeExpensiveValue, [a, b]);
Copy the code
The first argument to useMemo is a function that returns a value that is cached and used as the return value of useMemo. The second argument is an array dependency. If the value in the array changes, the function in the first argument is executed again. And cache the value returned by the function as the return value of useMemo.
Now that you know how to use useMemo, you can optimize the above example as follows:
function App() {
const [num, setNum] = useState(0);
function expensiveFn() {
let result = 0;
for (let i = 0; i < 10000; i++) {
result += i;
}
console.log(result)
return result;
}
const base = useMemo(expensiveFn, []);
return (
<div className="App">
<h1>Count: {num}</h1>
<button onClick={()= > setNum(num + base)}>+1</button>
</div>
);
}
Copy the code
Execute the above code, and now you can see that no matter how many times we click +1, it only prints 49995000 once, which means that expensiveFn only executes once, and it does what we want.
summary
UseMemo is mainly used to cache the results of functions with a large amount of computation, so as to avoid unnecessary double computation. Students who have used VUE may think that it has the same function as the calculation attributes in VUE.
But two other caveats
If an array of dependencies is not provided, useMemo evaluates the new values each time it renders.
If the computation amount is very small, you can also choose not to use useMemo, because this optimization will not be the point of performance bottleneck, but may use the error and cause some performance problems.
conclusion
For small projects, performance bottlenecks may be less encountered, since the amount of computation is small and the business logic is not complex. However, for large projects, performance bottlenecks may be encountered. However, there are many aspects of performance optimization: In terms of network, critical path rendering, packaging, images, caching and so on, we have to check by ourselves which aspects should be optimized. This article only introduces the tip of the iceberg in performance optimization: the optimization of React during operation.
- React optimization direction: reduce the number of render times; Reduce double counting.
- See the useCallback section for how to find out how React causes performance problems.
- If you split a component properly, you can also optimize the performance of the page. If you have a single component, then you need to change the props or state to the whole component, and you need to change the text of the whole component to a reconciliation. You can control more granular updates.
There are many other benefits to splitting components properly, such as easier maintenance, and this is the first step to learning the idea of componentization. Splitting components properly is an art, and if you don’t do it properly, it can lead to state confusion and more coding and more thinking.
Recommend the article
I only show you how to optimize functional components here. More React optimization tips can be read below:
- 21 React performance optimization tips
- Talk about the direction of React performance optimization
Afterword.
I am Taoweng, a thoughtful front-end er, want to know more about the front-end related, please pay attention to my public number: “Front-end Taoyuan”, if you want to join the exchange group to follow the public number reply “wechat” to pull you into the group