DEV Community

Cover image for Performance Optimization in React
Ugwuoke Adaeze
Ugwuoke Adaeze

Posted on • Edited on

Performance Optimization in React

Code Performance optimization is a set of strategies and techniques developers utilize to enhance the speed and efficiency of their software applications. There are various techniques software developers use to enhance performance, and we will be discussing a few of them below.

useMemo
useMemo is a React hook that caches/stores the result of a function so that the value doesn’t get recalculated unless its dependencies change. This is useful for expensive calculations that don’t need to run on every render.

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

const App = () => {
  const [count, setCount] = useState(0);
  const [input, setInput] = useState("");

  // Memoizing the result of the expensive calculation
  const memoizedValue = useMemo(() => {
    console.log("Calculating...");
    for (let i = 0; i < 1000000000; i++) {} // Simulate expensive logic
    return count * 2;
  }, [count]);

  return (
    <div>
      <h1>Memoization Example</h1>
      <p>Result: {memoizedValue}</p>

      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Enter something"
      />
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

In the code, Without useMemo() the expensive calculation would run on every render, even if only the input state changes.
With useMemo(): The calculation inside useMemo only runs when the count state changes. When input changes, React re-renders, but the cached value for memoizedValue is used, avoiding the recalculation of the expensive calculation.
This ensures performance optimization by minimizing unnecessary re-computations, especially when the expensive logic isn’t affected by every state update.

useCallback
useCallback is a React hook that memoizes a function so that it isn’t recreated every time the component re-renders. This is particularly useful when you pass functions as props to child components.

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

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

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

  // Memoizing the function to prevent re-creation
  const handleClick = useCallback(() => {
    console.log("Button clicked");
  }, []);

  return (
    <div>
      <h1>useCallback Example</h1>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment Count</button>
      <Child onClick={handleClick} />
    </div>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Without useCallback in the code, the handleClick function would be recreated every time the parent component re-renders, causing the Child to re-render unnecessarily.
With useCallback, the function is memoized, so the Child only re-renders when necessary.

useRef
useRef can play a key role in performance optimization by reducing unnecessary re-renders and improving efficiency. It allows developers to store mutable values and interact with the DOM without triggering component re-renders.
useRef can be utilized in different ways;
Avoids Re-renders by Storing Mutable Values: Unlike useState, updating a value stored in a useRef object does not trigger re-renders. This is particularly useful when you want to store values that change frequently (like form field focus states, counters, or previous values) but don’t affect the UI.

import { useRef } from 'react';

const ClickCounter = () => {
  const clickCount = useRef(0); // Persistent value across renders

  const handleClick = () => {
    clickCount.current += 1; // Updates without triggering re-render
    console.log(`Button clicked ${clickCount.current} times`);
  };

  return <button onClick={handleClick}>Click Me</button>;
};

export default ClickCounter;
Enter fullscreen mode Exit fullscreen mode

Here, the click count is updated without causing unnecessary re-renders of the component, improving performance.

Prevents Unnecessary Function Re-Creations: When used with event handlers or timers, useRef ensures that the function or value persists across renders. This prevents re-creating functions on each render, saving memory and execution time.

import { useRef, useEffect } from 'react';

const Timer = () => {
  const intervalRef = useRef(null);

  useEffect(() => {
    intervalRef.current = setInterval(() => {
      console.log('Tick');
    }, 1000);

    return () => clearInterval(intervalRef.current); // Cleanup on unmount
  }, []); // No dependency updates here

  return <div>Check the console for ticks!</div>;
};

export default Timer;
Enter fullscreen mode Exit fullscreen mode

Using useRef to store the interval ID ensures that the timer persists without needing to update the interval function or causing unnecessary component re-renders.

Efficient DOM Manipulation Without Re-renders

Using useRef to reference DOM elements avoids state updates (which would trigger re-renders) when all you need is to manipulate the DOM directly.
Example:

import { useRef } from 'react';

const AutoFocusInput = () => {
  const inputRef = useRef(null);

  const focusInput = () => {
    inputRef.current.focus(); // Directly interacts with the DOM
  };

  return (
    <div>
      <input ref={inputRef} type="text" />
      <button onClick={focusInput}>Focus Input</button>
    </div>
  );
};

export default AutoFocusInput;
Enter fullscreen mode Exit fullscreen mode

Explanation: Directly interacting with the DOM using useRef avoids unnecessary state updates and re-renders, improving performance.

Storing Previous State or Prop Values Without Triggering Re-renders

useRef can store the previous value of state or props without causing re-renders, unlike useState.
Example:

import { useState, useRef, useEffect } from 'react';

const PreviousState = () => {
  const [count, setCount] = useState(0);
  const prevCount = useRef(count); // Stores the previous value

  useEffect(() => {
    prevCount.current = count; // Updates without re-rendering
  }, [count]);

  return (
    <div>
      <p>Current Count: {count}</p>
      <p>Previous Count: {prevCount.current}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

export default PreviousState;
Enter fullscreen mode Exit fullscreen mode

In the code, the prevCount is stored using useRef to avoid triggering re-renders when updating the previous value.

Why useRef Improves Performance:
Prevents unnecessary re-renders: Storing values in useRef avoids costly UI updates when the component does not need to re-render.
Efficient DOM interactions: Manipulating the DOM directly with useRef avoids updating the component’s state.
Minimizes function re-creation: With useRef, event handlers and timers remain consistent across renders, reducing the overhead of re-creating functions.
When to Use useRef for Optimization:
When storing mutable values that don't need to trigger re-renders (e.g., counters, previous states).
When interacting with DOM elements directly without modifying component state.
When you need consistent references to functions or values (e.g., timers, event listeners).
Conclusion:
useRef is a powerful hook for performance optimization in React. By storing values that persist across renders without causing re-renders, it reduces unnecessary UI updates and enhances efficiency. It’s especially useful when managing DOM elements, maintaining previous state or props, and avoiding frequent function re-creation.

Memo
React memo is a higher-order component (HOC) that wraps your functional component. Memo is used to simulate pure components, that is components that don't rerender unless there is a change in the props or states. This helps to avoid unnecessary re-renders when the parent component updates.
Every time a component’s state or props change, React triggers a re-render to update the user interface. However, unnecessary re-renders can hurt performance, especially if the component tree is large.
Example; If ParentComponent passes a prop to ChildComponent, every time the parent re-renders, the child also re-renders, even if the child’s data hasn’t changed.
To memoize a component wrap it in a memo, it will not re-render when its parent component re-renders as long its props remain the same.

import { memo, useState } from 'react';

export default function MyApp() {
  const [name, setName] = useState('');
  const [address, setAddress] = useState('');
  return (
    <>
      <label>
        Name{': '}
        <input value={name} onChange={e => setName(e.target.value)} />
      </label>
      <label>
        Address{': '}
        <input value={address} onChange={e => setAddress(e.target.value)} />
      </label>
      <Greeting name={name} />
    </>
  );
}

const Greeting = memo(function Greeting({ name }) {
  console.log("Greeting was rendered at", new Date().toLocaleTimeString());
  return <h3>Hello{name && ', '}{name}!</h3>;
});
Enter fullscreen mode Exit fullscreen mode

In the code provided, memo enhances performance by preventing unnecessary re-renders of the Greeting component.
Without memo(), Greeting would re-render every time any part of the parent component (MyApp) re-renders, such as when address changes.
With memo(), Greeting only re-renders if the name prop changes, skipping re-renders when irrelevant state (like address) is updated.
This optimization reduces the rendering workload, improving the app's performance by minimizing unnecessary renders.
Memo() Optimizes Performance through:

  • Shallow Comparison of Props: On every render, memo() compares the current props with the previous props. If the props are the same (using shallow comparison), the component won’t re-render, reducing unnecessary rendering.
  • Avoids Re-renders in Child Components: Useful when a parent component frequently re-renders, but the child component’s props don’t change.

Profiler
The <Profiler /> component is a built-in React tool that measures the rendering behavior of components wrapped inside it. It records the response time of a component and logs each re-render with useful information such as render duration and the reason for re-renders (e.g., state changes or prop updates). Profiling adds some additional overhead, so it is disabled in the production build by default.
Profiler takes in two props, an id, and an onRender callback function.

<Profiler id="Sidebar" onRender={onRender}>
<Profiler/>
Enter fullscreen mode Exit fullscreen mode
  • id: A string identifying the part of the UI you are measuring, usually the name of the component being measured.
  • onRender: An onRender callback that React calls every time components within the profiled tree update. It receives information about what was rendered and how much time it took.
function onRender(id, phase, actualDuration, baseDuration, startTime, commitTime) {
  // Aggregate or log render timings...
}
Enter fullscreen mode Exit fullscreen mode

usage of Profiler:

  • The whole App can wrapped with the profiler as shown below
<Profiler id="App" onRender={onRender}>
  <App />
</Profiler>
Enter fullscreen mode Exit fullscreen mode
  • Wrapping specific components with the profiler
<App>
  <Profiler id="Sidebar" onRender={onRender}>
    <Sidebar />
  </Profiler>
  <PageContent />
</App>
Enter fullscreen mode Exit fullscreen mode
  • Nesting components with the profiler
<App>
    <Sidebar />
    <Profiler id="Content" onRender={onRender}>
    <Content>
      <Profiler id="Editor" onRender={onRender}>
        <Editor />
      </Profiler>
      <Preview />
    </Content>
  </Profiler>
</App>
Enter fullscreen mode Exit fullscreen mode

When to Use These Tools?

  1. Use React.memo: When you have functional components that receive unchanging props. Example: Display components that show static or rarely changing information.
  2. Use useMemo: When you need to cache expensive calculations to avoid unnecessary recomputations. Example: A search feature that filters large datasets.
  3. Use useCallback: When you pass functions as props to child components to avoid triggering re-renders. Example: Buttons or input handlers inside child components.
  4. Use <Profiler /> Component
  • During development: Use it to track performance bottlenecks early and ensure components aren’t rendering more than necessary.
  • In large applications: Use to analyze the impact of heavy components (e.g., data grids or charts).
  • Before releasing: Identify any slow renders or unnecessary re-renders before shipping your app.

Other methods of performance optimization include; code splitting and lazy for lazyloading.

Code splitting

Code-splitting is a feature that enables you to create multiple bundles that can be dynamically loaded at runtime. This basically breaking your application’s code into smaller chunks (bundles).
Instead of loading the entire app upfront, code splitting ensures that only the necessary chunks are loaded at a time—like loading a particular page or component on demand.
This reduces initial load time and improves performance, especially for large applications.
Code splitting can help your app “lazy-load” just the things that are currently needed by the user, which can significantly improve the speed and performance of your app. While you haven’t reduced the overall amount of code in your app, you’ve avoided loading code that the user may never need and reduced the amount of code needed during the initial load.

Lazy
lazy allows you to defer loading a component’s code until it is rendered for the first time. It returns a React component you can render in your tree. While the code for the lazy component is still loading, attempting to render it will be suspended. can be used to display a loading indicator while it’s loading.

import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

// Lazy load pages
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));

function App() {
  return (
    <Router>
      <Suspense fallback={<p>Loading page...</p>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
        </Routes>
      </Suspense>
    </Router>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

In the above code, we use lazy() to load the page components (Home, About, Contact) only when the user navigates to those routes and <suspense/> to show a loading message while the loads.

Happy Learning!!!

Top comments (0)