Optimizing Performance

Introduction

React is designed to be fast, but as applications grow in size and complexity, unnecessary re-renders and expensive calculations can affect performance. Functional components, while elegant, can sometimes rerender more often than necessary.

React provides two hooks—useMemo and useCallback—to help optimize performance by memoizing values and functions. These hooks prevent components from recomputing values or recreating functions on every render, reducing the rendering workload and improving efficiency.

In this article, we will explore:

  1. How useMemo helps memoize computed values.
  2. How useCallback helps memoize functions.
  3. How to avoid unnecessary re-renders using these hooks.
  4. Practical examples of expensive calculations and memoization.

What is useMemo?

useMemo is a hook that memoizes the result of a computation. It only recomputes the value when its dependencies change, preventing expensive calculations from running on every render.

Syntax

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
  • First argument: a function that returns a value.
  • Second argument: dependency array; the value recomputes only when dependencies change.

Basic Example of useMemo

import React, { useState, useMemo } from "react";

function ExpensiveComponent({ number }) {
  const expensiveCalculation = (num) => {
console.log("Calculating...");
let result = 0;
for (let i = 0; i < 1000000000; i++) {
  result += num;
}
return result;
}; const computedValue = useMemo(() => expensiveCalculation(number), [number]); return (
<div>
  <p>Computed Value: {computedValue}</p>
</div>
); } function App() { const [count, setCount] = useState(0); const [number, setNumber] = useState(5); return (
<div>
  <ExpensiveComponent number={number} />
  <button onClick={() => setCount(count + 1)}>Increment Count</button>
  <button onClick={() => setNumber(number + 1)}>Increment Number</button>
  <p>Count: {count}</p>
</div>
); }

Explanation

  • The expensive calculation runs only when number changes.
  • Updating count does not trigger recalculation, improving performance.

What is useCallback?

useCallback is a hook that memoizes a function, returning the same function instance unless its dependencies change. This prevents unnecessary function recreation and helps avoid re-rendering child components that rely on function props.

Syntax

const memoizedCallback = useCallback(() => {
  // function logic
}, [dependencies]);
  • First argument: the function to memoize.
  • Second argument: dependency array; the function is recreated only when dependencies change.

Example of useCallback

import React, { useState, useCallback } from "react";

function Button({ onClick, label }) {
  console.log("Button rendered");
  return <button onClick={onClick}>{label}</button>;
}

function App() {
  const [count, setCount] = useState(0);

  const handleClick = useCallback(() => {
setCount((prev) =&gt; prev + 1);
}, []); return (
&lt;div&gt;
  &lt;p&gt;Count: {count}&lt;/p&gt;
  &lt;Button onClick={handleClick} label="Increment" /&gt;
&lt;/div&gt;
); }

Explanation

  • Without useCallback, handleClick would be recreated on every render, causing Button to re-render unnecessarily.
  • useCallback ensures Button receives the same function instance unless dependencies change.

Why useMemo and useCallback Improve Performance

1. Memoizing Expensive Computations

useMemo avoids recomputing values that take significant time or resources.

2. Preventing Unnecessary Re-Renders

useCallback ensures child components depending on function props don’t re-render unless necessary.

3. Reducing Render Workload

By memoizing values and functions, React avoids executing computations or recreating functions on every render.


Avoiding Unnecessary Re-Renders

Example: Without Memoization

function Child({ onClick }) {
  console.log("Child rendered");
  return <button onClick={onClick}>Click</button>;
}

function Parent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  const handleClick = () => setCount(count + 1);

  return (
&lt;div&gt;
  &lt;Child onClick={handleClick} /&gt;
  &lt;input value={text} onChange={(e) =&gt; setText(e.target.value)} /&gt;
  &lt;p&gt;Count: {count}&lt;/p&gt;
&lt;/div&gt;
); }
  • Every time text changes, Parent re-renders.
  • Child re-renders because handleClick is a new function each time.

Example: Using useCallback to Avoid Re-Render

import React, { useState, useCallback } from "react";

const Child = React.memo(({ onClick }) => {
  console.log("Child rendered");
  return <button onClick={onClick}>Click</button>;
});

function Parent() {
  const [count, setCount] = useState(0);
  const [text, setText] = useState("");

  const handleClick = useCallback(() => setCount((prev) => prev + 1), []);

  return (
&lt;div&gt;
  &lt;Child onClick={handleClick} /&gt;
  &lt;input value={text} onChange={(e) =&gt; setText(e.target.value)} /&gt;
  &lt;p&gt;Count: {count}&lt;/p&gt;
&lt;/div&gt;
); }

Explanation

  • Child is wrapped with React.memo.
  • handleClick is memoized using useCallback.
  • Now Child does not re-render when text changes.

Expensive Calculation Memoization

Scenario: Fibonacci Sequence

Calculating Fibonacci numbers recursively is expensive. Memoization avoids recalculating the sequence on each render.

import React, { useState, useMemo } from "react";

function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

function FibonacciComponent() {
  const [num, setNum] = useState(35);
  const [count, setCount] = useState(0);

  const fibValue = useMemo(() => fibonacci(num), [num]);

  return (
&lt;div&gt;
  &lt;p&gt;Fibonacci({num}) = {fibValue}&lt;/p&gt;
  &lt;button onClick={() =&gt; setCount(count + 1)}&gt;Increment Count ({count})&lt;/button&gt;
  &lt;input type="number" value={num} onChange={(e) =&gt; setNum(Number(e.target.value))} /&gt;
&lt;/div&gt;
); }

Explanation

  • Fibonacci calculation is only recomputed when num changes.
  • Updating count does not trigger recomputation, improving performance.

Combining useMemo and useCallback

import React, { useState, useMemo, useCallback } from "react";

function ExpensiveList({ items, onItemClick }) {
  console.log("ExpensiveList rendered");
  return (
&lt;ul&gt;
  {items.map((item) =&gt; (
    &lt;li key={item} onClick={() =&gt; onItemClick(item)}&gt;{item}&lt;/li&gt;
  ))}
&lt;/ul&gt;
); } const MemoizedList = React.memo(ExpensiveList); function App() { const [count, setCount] = useState(0); const [input, setInput] = useState(""); const items = useMemo(() => {
console.log("Generating items");
return Array.from({ length: 1000 }, (_, i) =&gt; Item ${i});
}, []); const handleClick = useCallback((item) => {
console.log("Clicked:", item);
}, []); return (
&lt;div&gt;
  &lt;MemoizedList items={items} onItemClick={handleClick} /&gt;
  &lt;input value={input} onChange={(e) =&gt; setInput(e.target.value)} placeholder="Type here" /&gt;
  &lt;button onClick={() =&gt; setCount(count + 1)}&gt;Increment Count ({count})&lt;/button&gt;
&lt;/div&gt;
); }

Explanation

  • items array is memoized using useMemo.
  • handleClick function is memoized using useCallback.
  • MemoizedList does not re-render unnecessarily.

Best Practices for useMemo and useCallback

  1. Use only for expensive computations or frequent re-renders
    • Don’t overuse; memoization has overhead.
  2. Keep dependency arrays accurate
    • Include all values used inside the memoized function.
  3. Combine with React.memo
    • Helps prevent child component re-renders.
  4. Avoid inline functions for props
    • Use useCallback to prevent function recreation.
  5. Profile before optimizing
    • Use React DevTools profiler to identify bottlenecks.

Common Pitfalls

  1. Over-memoization
    • Memoizing cheap calculations or functions can reduce performance.
  2. Incorrect dependencies
    • Omitting dependencies can lead to stale values.
  3. Using memoization unnecessarily
    • For small apps or lightweight calculations, memoization is often unnecessary.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *