DEV Community

Cover image for Master React Optimization Technique
Arkajit Roy
Arkajit Roy

Posted on

Master React Optimization Technique

In today's fast-paced web landscape, a React application's performance is paramount. A sluggish app can lead to frustrated users and lost conversions. Fortunately, React provides a plethora of built-in features and libraries to streamline your application and deliver a seamless experience. Here are 7 optimization techniques that will elevate your React app to production-grade quality

01. Component Memoization

To achieve memoization we can employee React.memo which is a higher-order component (HOC) that prevents unnecessary re-renders of functional components by memoizing the component output based on its props. It re-renders only if the props or the component's state change.

import React from 'react';

const MyComponent = React.memo(({ value }) => {
  console.log('Rendered');
  return <div>{value}</div>;
});

export default MyComponent;

Enter fullscreen mode Exit fullscreen mode

Using useCallback and useMemo for Stable Reference

Similar to useMemo but for functions, useCallback prevents unnecessary function recreation when its dependencies haven't changed. This is particularly helpful for callback functions passed as props.

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

const MyComponent = ({ items }) => {
  const [count, setCount] = useState(0);

  const calculateTotal = useMemo(() => {
    return items.reduce((total, item) => total + item.price, 0);
  }, [items]);

  const increment = useCallback(() => {
    setCount((prevCount) => prevCount + 1);
  }, []);

  return (
    <div>
      <p>Total: {calculateTotal}</p>
      <button onClick={increment}>Increment: {count}</button>
    </div>
  );
};

export default MyComponent;

Enter fullscreen mode Exit fullscreen mode

02. Code Splitting Technique

Lazy loading allows you to load components or modules only when they're needed. This reduces the initial bundle size and improves load times, especially for complex applications. React's built-in React.lazy and Suspense components facilitate this process.

import React, { lazy, Suspense } from 'react';

const MyLazyComponent = lazy(() => import('./MyLazyComponent'));

function MyComponent() {
  return (
    <div>
      <button onClick={() => import('./MyLazyComponent')}>Load Lazy Component</button>
      <Suspense fallback={<div>Loading...</div>}>
        <MyLazyComponent />
      </Suspense>
    </div>
  );
}

Enter fullscreen mode Exit fullscreen mode

03. Efficient Event Handling

For frequently triggered events, consider Throttling or debouncing techniques to reduce the number of function calls. Throttling ensures the function executes at most once within a specified time interval, while debouncing only executes it after a period of inactivity.

Debouncing

import React, { useState } from 'react';
import { debounce } from 'lodash';

const Search = () => {
  const [query, setQuery] = useState('');

  const handleSearch = debounce((event) => {
    setQuery(event.target.value);
    // Perform search operation
  }, 300);

  return <input type="text" onChange={handleSearch} />;
};

export default Search;
Enter fullscreen mode Exit fullscreen mode

Throttling

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

const AnimationComponent = () => {
  const ref = useRef();

  useEffect(() => {
    const animate = () => {
      // Update animation frame
      requestAnimationFrame(animate);
    };
    requestAnimationFrame(animate);
  }, []);

  return <div ref={ref}>Animating...</div>;
};

export default AnimationComponent;
Enter fullscreen mode Exit fullscreen mode

04. Rendering Long Lists (Virtualized Lists)

When dealing with extensive lists, list virtualization becomes crucial. It renders only the visible items on the screen, significantly improving performance and reducing DOM manipulation. Popular libraries like react-window and react-virtualized offer efficient solutions

import React from 'react';
import { FixedSizeList as VirtualizedList } from 'react-window';

const ListItem = ({ index, style }) => (
  <div style={style}>Row {index}</div>
);

const MyList = ({ items }) => (
  <VirtualizedList
    height={150}
    itemCount={items.length}
    itemSize={35}
    width={300}
  >
    {ListItem}
  </VirtualizedList>
);

export default MyList;
Enter fullscreen mode Exit fullscreen mode

05. Track Performance Bottlenecks

Profiling is the cornerstone of optimization. Use React DevTools Profiler to identify performance bottlenecks in your components. It pinpoints areas consuming excessive rendering time, guiding your optimization efforts.

import React, { Profiler } from 'react';

const onRenderCallback = (
  id, // the "id" prop of the Profiler tree that has just committed
  phase, // either "mount" (if the tree just mounted) or "update" (if it re-rendered)
  actualDuration, // time spent rendering the committed update
  baseDuration, // estimated time to render the entire subtree without memoization
  startTime, // when React began rendering this update
  commitTime, // when React committed this update
  interactions // the Set of interactions belonging to this update
) => {
  console.log('Render time:', actualDuration);
};

const App = () => (
  <Profiler id="App" onRender={onRenderCallback}>
    <MyComponent />
  </Profiler>
);

export default App;
Enter fullscreen mode Exit fullscreen mode

06. Optimize Context Usage

Using useContextSelector from the use-context-selector library can help in avoiding unnecessary re-renders when using context.

import React from 'react';
import { useContextSelector } from 'use-context-selector';

const CountContext = React.createContext();

const Display = () => {
  const count = useContextSelector(CountContext, state => state.count);
  return <div>{count}</div>;
};

const App = () => {
  const [count, setCount] = React.useState(0);
  const contextValue = { count, setCount };

  return (
    <CountContext.Provider value={contextValue}>
      <Display />
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </CountContext.Provider>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

07. Usage of Fragment Components

Avoid unnecessary DOM nodes by wrapping JSX elements with React.Fragment. This prevents creating extra

elements for layout purposes.
import React, { Fragment } from 'react';

function MyComponent() {
  return (
    <Fragment>
      <p>Item 1</p>
      <p>Item 2</p>
    </Fragment>
  );
}

// OR

function MyComponent() {
  return (
    <>
      <p>Item 1</p>
      <p>Item 2</p>
    </>
  );
}

Top comments (9)

Collapse
 
thethirdrace profile image
TheThirdRace

Profiler

I didn't know that one, it's very instructive 👍

Memoization

I would be very careful before recommending memoization. While it's more performant in rare cases, 99.9% of the time it's premature optimization.

It's like going to the carnival and you get some tickets for the rides.

  1. If you get 10 tickets, you definitely need to optimize because you have a low budget and you can't ride all the rides you want to do with your tickets
  2. If you get 100K tickets, you definitely DO NOT need to optimize, it's a moot point because no matter what you try, you're still gonna have plenty of tickets by the end of the day and you can't keep them for tomorrow 🤷‍♂️

You only rarely see case #1, so memoizing should not be your default reflex.

Note that the useCallback example serves no purpose whatsoever. If your component re-render, it will re-render the button too, nothing will be lost by not using it. This is the perfect example of premature optimization. The code is less readable without any gain to show for it.

Optimize context usage

This is greatly related to premature optimization from the previous point.

If you need that kind of optimization, you might have a problem in how the context architected. Sure you get re-renders with the normal context, but if it's a problem then there's something else that you can do to prevent it.

Loading 2 extra libraries for the rare case you'll need it is not an optimization. At this point, if you really need something like this, might as well go for zustand that will give you so much more than this and will be mostly tree-shakeable. You might even load less JS in the end with Zustand, not that you should need it in most cases.

Efficient event handling

Again, I would be careful speaking about optimization if your examples are not optimized.

The debounce example is loading the whole lodash library... lodash is not tree-shaking like it's supposed to and this kind of import is bringing a whole lot of JS for nothing. Check on the lodash npmjs.com page how to adjust the importing correctly.

The throttle example is not a throttle... It's the equivalent of a setInterval, which is not the same process as throttling. If you were to mount/unmount that component repeatedly in a few milliseconds interval, you'd get an inordinate amount of calls to requestAnimationFrame. Throttling would not allow this.

Collapse
 
praneshchow profile image
Pranesh Chowdhury

I will learn more about the 'Fragment' component. It is useful when we want to return multiple elements from a components render method without wrapping them in an unnecessary div. However, I mostly use container components for grouping. Thanks.

Collapse
 
leandro_nnz profile image
Leandro Nuñez

Concise and effective. Great reading. Thanks!

Collapse
 
emmanuelayinde profile image
The Great SoluTion

Great article. I learnt about new tools.
Thanks for sharing

Collapse
 
vjnvisakh profile image
Visakh Vijayan

Nice read. I didn't quite understand why we need useContextSelector. Shouldn't the components that use the context re-render. Isn't that the advantage?

Collapse
 
oculus42 profile image
Samuel Rouse

Context causes a re-render for every update, even if the update doesn't cause the component to change. Imagine storing the data from a Contact form in Context:

If you update the name, the email component doesn't need to re-render... nothing relevant changed for email. If you use context directly, you will re-render email when name changes.

useContextSelector will run a smaller function to determine if the context update caused a relevant change, and avoids re-rendering components when the data they use hasn't changed.

Collapse
 
cookiemonsterdev profile image
Mykhailo Toporkov 🇺🇦

Or just use new react compiler, even though it is in beta)))

Collapse
 
markuz899 profile image
Marco

I might add

the instruction for the appropriate use of useEffect with the appropriate dependencies, very often underestimated

Collapse
 
maxarias profile image
Maximiliano Arias

Why useMemo for components and not useCallback?

Some comments may only be visible to logged-in visitors. Sign in to view all comments.