π Introduction
As your React application grows in complexity, you might notice that its performance starts to degrade. More features, more components, and more rendering logic can lead to noticeable slowdowns if not properly managed.
π Why does this happen?
- Unnecessary re-renders of components.
- Inefficient state management.
- Heavy computations running repeatedly.
- Poor memory handling in effects and event listeners.
In todayβs article, weβll dive into how to identify and optimize performance bottlenecks in your React application. By the end, youβll have a solid understanding of:
- How React rendering works.
- Techniques to reduce unnecessary renders.
- Proper use of memoization (
React.memo
,useMemo
, anduseCallback
).
Letβs get started! π¦
π§ Understanding Component Rendering
Before diving into optimizations, itβs important to understand why and when React components re-render. Every re-render involves recalculating the Virtual DOM and applying changes to the real DOMβan expensive process we want to minimize.
β‘ When does a component re-render?
1οΈβ£ Parent Component Re-renders: When a parent component re-renders, its child components will also re-renderβeven if their props havenβt changed.
2οΈβ£ State Changes: When a componentβs local state updates using useState
.
3οΈβ£ Context Changes: If a component consumes context (useContext
) and the context value changes.
π Key Takeaway: To improve performance, focus on reducing unnecessary renders triggered by these factors.
In the next sections, weβll explore actionable strategies to achieve this.
π¦ Using the children
Prop Instead of Direct Composition
This technique is particularly useful when dealing with heavy child components that shouldnβt re-render unnecessarily.
π The Problem with Direct Composition
function ParentComponent() {
return (
<div>
<h1>Parent Component</h1>
<p>This is the parent component</p>
<ChildComponent />
</div>
);
}
function ChildComponent() {
// Some slow calculation
slowFunction();
return (
<div>
<h1>Child Component</h1>
<p>This is the child component</p>
</div>
);
}
// Usage:
<ParentComponent />
π The Issue: Every time ParentComponent
re-renders (even due to unrelated state changes of the child component), ChildComponent
also re-renders, repeating expensive calculations.
β
The Solution: Using the children
Prop
function ParentComponent({ children }) {
return (
<div>
<h1>Parent Component</h1>
<p>This is a component</p>
{children}
</div>
);
}
function ChildComponent() {
// Some slow calculation
slowFunction();
return (
<div>
<h1>Child Component</h1>
<p>This is the child component</p>
</div>
);
}
// Usage:
<ParentComponent>
<ChildComponent />
</ParentComponent>
π What Changed?
-
ChildComponent
is passed as a child prop, which ensures it doesnβt re-render unnecessarily whenParentComponent
updates.
π‘ Pro Tip: Use this pattern for components that are computationally heavy or rely on static props.
βοΈ Memoization: Reducing Unnecessary Re-Renders
π§ What is Memoization?
Memoization is an optimization technique used to reduce unnecessary computation by caching the results of expensive function calls and reusing them when the same inputs occur again.
In simple terms:
- If a function is repeatedly called with the same arguments, instead of recalculating the result every time, we store the result in memory and fetch it from there.
π’ Memoization Example: Fibonacci Sequence
Letβs take a classic example: Calculating the Nth Fibonacci number.
Without Memoization:
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
console.log(fibonacci(40)); // Slow computation
π Problem: Every call recalculates previous Fibonacci numbers, even if they were already computed before. This leads to an exponential number of function calls.
With Memoization:
function fibonacci(n, cache = {}) {
if (n in cache) return cache[n]; // If the result is already cached, return it
if (n <= 1) return n; // Base case: return n if it's 0 or 1
// Otherwise, calculate and cache the result
cache[n] = fibonacci(n - 1, cache) + fibonacci(n - 2, cache);
return cache[n]; // Return the cached result
}
console.log(fibonacci(40)); // This will now run faster by reusing cached results
π οΈ How does this apply in React?
React leverages memoization to optimize performance:
-
React.memo
β Memoizes components to prevent unnecessary re-renders. -
useMemo
β Memoizes expensive computations to prevent recalculating results unnecessarily. -
useCallback
β Memoizes functions to prevent them from being recreated on every render.
Letβs dive into each.
βοΈ Using React.memo
for Component Memoization
π What is React.memo
?
React.memo
is a higher-order component (HOC) that caches the rendered result of a component and prevents it from re-rendering if its props havenβt changed.
import React, { memo, useState } from 'react';
const SlowComponent = memo(({ message }) => {
someSlowFunction();
return (
<div>
<p>{message}</p>
</div>
);
});
function App() {
const [message, setMessage] = useState("Hello world");
const [isLoading, setIsLoading] = useState(false);
const [visibleItems, setVisibleItems] = useState([]);
return (
<div>
<Child message={message} />
</div>
);
}
π When is React.memo
NOT Effective?
While React.memo
works well for primitive props (e.g., string
, number
, boolean
), it fails when props are objects or functions because:
- Objects and functions are reference types in JavaScript.
- Every render creates new object and function references even if their values remain the same.
β οΈ Example of the Problem:
import React, { memo, useState } from 'react';
const Child = memo(({ data, sayHello }) => {
someSlowFunction();
return (
<div>
<p>{data.message}</p>
<button onClick={sayHello}>Say hello</button>
</div>
);
});
function App() {
const [message, setMessage] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [visibleItems, setVisibleItems] = useState([]);
const data = { message }; // New object every render
function sayHello() {
console.log("Hello world");
}
return (
<div>
<Child data={data} sayHello={sayHello} />
</div>
);
}
π Problem Explanation:
-
data
is re-created on every render because objects (functions) in JavaScript are compared by reference. - As a result,
Child
will always re-render, even thoughdata
's value hasnβt changed.
β
Solution: Use useMemo
to Stabilize Object References
π οΈ What is useMemo
?
useMemo
is a hook that memoizes the result of an expensive computation and only recalculates when its dependencies change.
π When to Use useMemo
?
- When you have an expensive computation inside your component (e.g., large array processing).
- When you want to stabilize object references to avoid unnecessary renders.
import React, { memo, useState, useMemo } from 'react';
const Child = memo(({ data, sayHello }) => {
someSlowFunction();
return (
<div>
<p>{data.message}</p>
<button onClick={sayHello}>Say hello</button>
</div>
);
});
function App() {
const [message, setMessage] = useState("hello world");
const [isLoading, setIsLoading] = useState(false);
const [visibleItems, setVisibleItems] = useState([]);
function sayHello() {
console.log("Hello world");
}
const data = useMemo(() => ({ message: message }), [message]);
return (
<div>
<Child data={data} sayHello={sayHello} />
</div>
);
}
π The useMemo partly fixes the issue, but we still need to fix the issue related to the function sayHello.
β
Solution: Use useCallback
for Functions
π οΈ What is useCallback
?
useCallback
is a React hook used to memoize functions so they donβt get recreated on every render.
import React, { memo, useState, useCallback } from 'react';
const Child = memo(({ data, sayHello }) => {
someSlowFunction();
return (
<div>
<p>{data.message}</p>
<button onClick={sayHello}>Say hello</button>
</div>
);
});
function App() {
const [message, setMessage] = useState("hello world");
const [isLoading, setIsLoading] = useState(false);
const [visibleItems, setVisibleItems] = useState([]);
const sayHello = useCallback(() => {
console.log("Hello world");
}, []);
const data = useMemo(() => ({ message: message }), [message]);
return (
<div>
<Child data={data} sayHello={sayHello} />
</div>
);
}
π How and When to Optimize?
π οΈ When Should You Optimize?
- Only optimize when you encounter a noticeable performance bottleneck.
- Premature optimization can lead to overly complex code.
π Use React Profiler
- Analyze performance bottlenecks using the React Profiler in React DevTools.
- Look for "wasted renders" or "long rendering times."
π Conclusion
In this article, we covered:
β
Understanding React rendering behavior.
β
Using children
to reduce re-renders.
β
Optimizing with React.memo
, useMemo
, and useCallback
.
β When and how to perform optimizations.
By applying these best practices thoughtfully, youβll ensure your app remains fast, and scalable as it grows.
Stay tuned for more optimization tips in the next articles! π
Top comments (1)
Hey there! I hope you enjoyed the article. One thing I forgot to mention is that it's also important to clean up useEffect when needed, especially for things like timers, event listeners, or network requests, to avoid memory leaks and unnecessary operations.Additionally, the new React 19 compiler has introduced several optimizations to handle unnecessary renders more efficiently, so you might not need to do as much memoization as before.
Thank you.