It has been several months since I started working with React. During this time, I have been puzzled by the issue of how to avoid 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 changes are 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 output because the color
state has changed, which indirectly indicates 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 be updated 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 state changes, 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 example from the previous section 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 look at the following example first.
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 content is entered in the input. To solve this, we just need to wrap the parent component's fn function 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 above 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>;
});