Common Methods for Optimizing Next.js Rendering Performance
"TLDR: This article provides a detailed introduction to common rendering performance optimization methods in Next.js, including using `useCallback` to cache function creation and avoid unnecessary re-renders of child components, leveraging `useMemo` to cache complex calculation results and reduce redundant computations, properly configuring the dependency array of `useEffect` to control the timing of side effect execution, and correctly using `Suspense` to provide friendly waiting prompts during asynchronous component loading. These technical approaches can effectively enhance the performance of React applications."
useCallback
Background: When a function component re-renders, the entire function body is re-executed, causing functions defined inside to be recreated:
function MyComponent() {
const handleClick = () => {
console.log("clicked");
};
return <button onClick={handleClick}>Click me</button>;
}
- Every time the component re-renders, the
handleClickcreated is a new function object - Recreating function objects generally doesn't consume much, so it's usually not a big deal
- However, if this function is passed to a child component, the change in the function will cause the child component to re-render, which is costly
const Child = React.memo(({ onClick }) => {
console.log("Child render");
return <button onClick={onClick}>Child component button</button>;
});
function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => console.log("clicked");
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Add 1</button>
<Child onClick={handleClick} />
</div>
);
}
- The Child component depends on the
onClickfunction; the recreation ofhandleClickcauses the Child component to re-render - Solution: cache the function creation
const handleClick = useCallback(() => {
console.log("clicked");
}, []);
- A new function is only generated when the content in
[]changes
useMemo
As mentioned above, recreating functions generally doesn't consume much, but if the function's computation logic is complex, the computation itself can be expensive. In that case, you can use useMemo to cache the function's computed result.
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemocaches a computed result- It only recomputes when the dependencies
[a, b]change - It avoids recomputing complex logic
useEffect
When a function component renders, it doesn't just render JSX — it also needs to perform other operations (data fetching, etc.)
useEffect(() =>{...}): executes after every renderuseEffect(() =>{...}, []): only executes after the first renderuseEffect(() =>{...}, [a, b]): executes after every render wheneveraorbchanges
useEffect(() => {
const handleResize = () => console.log(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []); // Empty array → registers on mount, cleans up on unmount
- The function body is what runs after the render is complete
- If the rendered component is unmounted and needs to do some cleanup, you can encapsulate that in the return statement
Suspense
Used for asynchronous rendering:
// app/page.tsx
import ResourceList from './ResourceList';
export default function Page() {
return (
<Suspense fallback={<div>Loading resources...</div>}>
<ResourceList />
</Suspense>
);
}
// app/ResourceList.tsx
async function ResourceList() {
const resources = await fetch('https://api.example.com/resources').then(res => res.json());
return (
<ul>
{resources.map(r => <li key={r.id}>{r.title}</li>)}
</ul>
);
}
- It's only useful when the wrapped component is an async component
- Placing a
<div>directly inside<Suspense>is useless (I've been using it wrong all along)