It has been several months since I started working with React. During this time, I have been puzzled by the issue of avoiding unnecessary re-rendering. So today, let's talk about this topic.
Before discussing performance optimization, let's first talk about why React re-renders.
Why does React re-render?#
State change is one of the two reasons for updates within the React tree.
import { useState } from "react";
const App = () => {
let [color, setColor] = useState("red");
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<ExpensiveTree />
</div>
);
};
const ExpensiveTree = () => {
let now = performance.now();
while (performance.now() - now < 100) {
// Delay
}
console.log('render');
return <p>I am a very slow component tree.</p>;
};
Clearly, whenever we enter content in the input, console.log('render')
is outputted because the color
state has changed, indicating that it has no relation to props.
This kind of overhead is unreasonable, so we will optimize it next.
Performance Optimization#
Method 1: State Extraction#
We know that React follows a unidirectional data flow, so we only need to extract the state.
import { useState } from "react";
const App = () => {
return (
<div>
<Input />
<ExpensiveTree />
</div>
);
};
const ExpensiveTree = () => {
let now = performance.now();
while (performance.now() - now < 100) {
// Delay
}
console.log("render");
return <p>I am a very slow component tree.</p>;
};
const Input = () => {
let [color, setColor] = useState("red");
return <input value={color} onChange={(e) => setColor(e.target.value)} />;
}
Method 2: memo#
React.memo is a higher-order component that turns the wrapped component into a pure component, meaning that it will only update if its props change.
import { memo, useState } from "react";
const App = () => {
let [color, setColor] = useState("red");
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<ExpensiveTree />
</div>
);
};
const ExpensiveTree = memo(() => {
let now = performance.now();
while (performance.now() - now < 100) {
// Delay
}
return <p>I am a very slow component tree.</p>;
})
Method 3: react children#
Since App does not undergo a state change, ExpensiveTree avoids unnecessary re-rendering.
import { FC, PropsWithChildren, useState } from "react";
const App = () => {
return (
<ColorWrapper>
<ExpensiveTree />
</ColorWrapper>
);
};
const ColorWrapper: FC<PropsWithChildren> = ({ children }) => {
let [color, setColor] = useState("red");
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
{children}
</div>
);
};
const ExpensiveTree = () => {
let now = performance.now();
while (performance.now() - now < 100) {
// Delay
}
return <p>I am a very slow component tree.</p>;
};
Using useMemo and useCallback#
useMemo#
useMemo is similar to Computed
in Vue. It only recalculates the value when the dependencies change.
This way, when the input changes, dirtyWork will not be repeatedly executed.
import { useMemo, useState } from "react";
const App = () => {
let [color, setColor] = useState("red");
const [number,setNumber] = useState(0)
const dirtyWork = useMemo(() => {
console.log('Doing a lot of work');
return number
},[number])
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<h1>{dirtyWork}</h1>
</div>
);
};
Additionally, we can also modify the previous example using useMemo.
import { memo, useMemo, useState } from "react";
const App = () => {
let [color, setColor] = useState("red");
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
{useMemo(
() => (
<ExpensiveTree />
),
[]
)}
</div>
);
};
const ExpensiveTree = () => {
let now = performance.now();
while (performance.now() - now < 100) {
// Delay
}
return <p>I am a very slow component tree.</p>;
};
useCallback#
Let's take a look at the following example:
import { FC, memo, useState } from "react";
const App = () => {
let [color, setColor] = useState("red");
const fn = ()=> {
console.log('hahaha');
}
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<ExpensiveTree fn={fn}/>
</div>
);
};
const ExpensiveTree:FC<{fn:()=>void}> = memo(({fn}) => {
let now = performance.now();
while (performance.now() - now < 100) {
// Delay
}
console.log('render'); // Still gets updated
return <p>I am a very slow component tree.</p>;
})
We can see that even though ExpensiveTree is wrapped with memo, it still gets updated when we enter content in the input. To solve this, we can wrap the fn function in the parent component with useCallback.
Therefore, useCallback is generally used when passing functions to child components. Let's rewrite the above example using useCallback:
import { FC, memo, useCallback, useState } from "react";
const App = () => {
let [color, setColor] = useState("red");
const fn = useCallback(()=> {
console.log('hahaha');
},[])
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<ExpensiveTree fn={fn}/>
</div>
);
};
const ExpensiveTree:FC<{fn:()=>void}> = memo(({fn}) => {
let now = performance.now();
while (performance.now() - now < 100) {
// Delay
}
console.log('render');
return <p>I am a very slow component tree.</p>;
})
You may notice that useCallback is actually syntactic sugar for useMemo. The previous example can also be rewritten using useMemo:
import { FC, memo, useMemo, useState } from "react";
const App = () => {
let [color, setColor] = useState("red");
const fn = useMemo(() => {
return () => console.log("hahaha");
}, []);
return (
<div>
<input value={color} onChange={(e) => setColor(e.target.value)} />
<ExpensiveTree fn={fn} />
</div>
);
};
const ExpensiveTree: FC<{ fn: () => void }> = memo(({ fn }) => {
let now = performance.now();
while (performance.now() - now < 100) {
// Delay
}
console.log("render");
return <p>I am a very slow component tree.</p>;
});