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

5 1 1 1 2

๐Ÿš€ 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! ๐Ÿš€

๐Ÿ‘‹ While you are here

Reinvent your career. Join DEV.

It takes one minute and is worth it for your career.

Get started

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.

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

๐Ÿ‘‹ Kindness is contagious

Dive into an ocean of knowledge with this thought-provoking post, revered deeply within the supportive DEV Community. Developers of all levels are welcome to join and enhance our collective intelligence.

Saying a simple "thank you" can brighten someone's day. Share your gratitude in the comments below!

On DEV, sharing ideas eases our path and fortifies our community connections. Found this helpful? Sending a quick thanks to the author can be profoundly valued.

Okay