DEV Community

Cover image for πŸš€ React Best Practices for Scalable Frontends: Part 4 – Performance Optimization and Efficient Rendering
El Mahfoud Bouatim
El Mahfoud Bouatim

Posted on

πŸš€ React Best Practices for Scalable Frontends: Part 4 – Performance Optimization and Efficient Rendering

πŸš€ 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, and useCallback).

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 />

Enter fullscreen mode Exit fullscreen mode

πŸ” 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>

Enter fullscreen mode Exit fullscreen mode

πŸ”‘ What Changed?

  • ChildComponent is passed as a child prop, which ensures it doesn’t re-render unnecessarily when ParentComponent 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

Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ 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


Enter fullscreen mode Exit fullscreen mode

πŸ› οΈ 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>
  );
}

Enter fullscreen mode Exit fullscreen mode

πŸ“ 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:

  1. Objects and functions are reference types in JavaScript.
  2. 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>
  );
}

Enter fullscreen mode Exit fullscreen mode

πŸ” 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 though data'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>
  );
}

Enter fullscreen mode Exit fullscreen mode

πŸ‘‰ 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>
  );
}

Enter fullscreen mode Exit fullscreen mode

πŸ“Š 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)

Collapse
 
el_mahfoudbouatim_b502a2 profile image
El Mahfoud Bouatim

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.