Optimizing React The Right Way For Blazing Fast Apps



This content originally appeared on DEV Community and was authored by Fenigma

Building fast, responsive React applications goes beyond writing functional code but also includes ensuring it performs optimally. Users expect pages to load instantly, scroll smoothly, and respond without lag, no matter how complex the app becomes.

However, as React apps grow, bundle sizes increase, components re-render unnecessarily, and heavy lists slow down performance. The good news is that React provides multiple powerful optimization techniques that keep apps snappy while maintaining clean, modern code.

The first step toward faster apps starts before the browser even runs your code: the size of the bundle itself. If your JavaScript bundle is bloated, performance issues appear before your app has a chance to load. That’s where bundle optimization comes in.

Bundle Optimization

When you run a build, tools like Webpack, Vite, or Rollup package the source code and its dependencies into one or more bundles. These bundles are the JavaScript files that the browser downloads and uses to load an app’s functionality.

Bundle optimization techniques are processes that reduces the size and improves the efficiency of these bundles. Lighter bundles produce faster apps, and better performance scores on core web vitals like LCP (Largest Contentful Paint) and TTI (Time to Interactive).

The following methods can be used to improve React apps’ bundle size:

Tree Shaking: This is a form of dead code elimination. Unused import and functions add to the bundle size of a React application. Tree shaking removes any code that is imported but never used.

Especially with ES6 modules, imports can be limited to specific functions needed in a project instead of the entire library. Moreover, when ES6 module imports are used, bundlers like Vite/Webpack automatically perform tree shaking. (CommonJS doesn’t get tree-shaken).

Using the date-fns library for an example

// ❌ Avoid importing the entire date-fns library

// ✅ Import only the needed functions
import format from 'date-fns/format';
import differenceInDays from 'date-fns/differenceInDays';

const today = new Date();
const nextWeek = new Date(today);
nextWeek.setDate(today.getDate() + 7);

console.log(format(today, 'yyyy-MM-dd')); // 2025-08-20
console.log(differenceInDays(nextWeek, today)); // 7

Code Splitting: Instead of shipping one massive JavaScript file, split your bundle into smaller chunks that can be accessed independently when they are needed.

Although code splitting can also be done at the router level, splitting routes with React Router, another practical approach is with React.lazy and Suspense.

const HeavyChart = React.lazy(() => import("./Chart"));

function Dashboard() {
  return (
    <Suspense fallback={<p>Loading chart...</p>}>
      <HeavyChart />
    </Suspense>
  );
}

Explanation:

  • React.lazy ensures that the Chartcomponent is dynamically imported only when it is actually needed.
  • Instead of bundling Chart into the main JavaScript file, it creates a separate chunk (code splitting) which reduces the initial bundle size.
  • Since React.lazy loads asynchronously, Suspense acts as a wrapper that shows a fallback UI (<p>Loading chart...</p>) while the HeavyChart component is being fetched.

Once your app is running, React’s rendering process becomes the next bottleneck. That’s where memoization techniques help keep your UI responsive.

💡NOTE: Other bundle optimization techniques not covered in this article include code minification, caching, bundle analyzers, and dynamic imports.

React Performance Boost with Memoization (React.memo, useMemo, and useCallback)

As React apps grow, one of the most common performance issues is unnecessary re-renders. Every time state or props change, React re-renders components to synchronize the UI. While that’s great, often React ends up doing a lot of work when nothing really changed.

Memoization helps React to remember the result of a function or component so React doesn’t re-render it unless something important changes. This can be achieved with three of React’s built in tools.

React.memo: By default, when a parent component re-renders, all its children also re-render — even if their props didn’t change. To fix this performance issue, wrapping such children component in React.memo would, in effect, tell react: “Only re-render this component if its props changed even if its parent is re-rendered”

Example:

import React from "react";

// Child component wrapped in React.memo
const UserCard = React.memo(function UserCard({ name }) {
  return <div>{name}</div>;
});

export default function App() {
  const [count, setCount] = React.useState(0);

  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Increase: {count}</button>
      <p>{count}</p>

      {/* UserCard won’t re-render when count changes */}
      <UserCard name="Fenigma" />
    </div>
  );
}

Now, clicking the button re-renders the parent but UserCard doesn’t re-render since its props (name) stayed the same.

useMemo: Sometimes memory expensive calculations like filtering a big list are run inside a component. If the component re-renders and recalculates every time, vital processing capacity wastes.

useMemo caches the result of a calculation until its dependencies change.

Example:

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

function App() {
  const [count, setCount] = useState(0);
  const [items] = useState(Array.from({ length: 10000 }, (_, i) => i));

  // Expensive computation (sum of numbers)
  const total = useMemo(() => {
    console.log("Calculating...");
    return items.reduce((a, b) => a + b, 0);
  }, [items]); // Recalculates only if `items` changes

  return (
    <div>
      <p>Total: {total}</p>
      <button onClick={() => setCount(count + 1)}>Increase {count}</button>
    </div>
  );
}

Explanation: This component calculates the sum of 10,000 numbers once, shows it on screen, and lets you increase counter without re-running the expensive calculation each time.

useCallback: In React, functions are re-created every time a component renders. Passing these “new” functions down as props can cause child components to re-render.

useCallback solves this by caching the function itself until its dependencies change.

**Example:

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

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

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

  // Without useCallback, a new function is created every render
  const handleClick = useCallback(() => {
    console.log("Clicked");
  }, []); // Stays the same unless dependencies change

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>Increase</button>
      <Child onClick={handleClick} />
    </div>
  );
}

CAVEAT: Overusing memoization can hurt performance. React.memo adds comparison overhead. Good rule: Use it when re-renders are expensive, not everywhere.

Memoization reduces wasted work inside components, but what if your app has to render thousands — or even hundreds of thousands — of items at once? That’s a different challenge altogether. In such cases, list windowing is the key to keeping scrolling smooth and interactions snappy.

List Windowing

What is List Windowing?
When dealing with large lists with thousands of rows or grid cells, rendering everything at once kills performance and slows down React apps. List windowing is a technique that fixes this bottleneck by splitting large data into smaller chunks and rendering only what is visible in the viewport.

This approach dynamically updates the UI with newly rendered items as users scroll through a responsive and fully performant interface.

React Virtualized, React Window (a light-weight alternative with smaller bundle size and simpler API), and React-Window-infinite-loader are node packages for implementing List Windowing in various contexts.

Example with React Virtualized

To use React Virtualized for optimizing a list, install the library with the command:

npm install react-virtualized --save

The component below shows a simple example of the library in use:

import { useState, useCallback } from "react"; 
import { List, AutoSizer } from "react-virtualized";

// Utility function to generate a list of items (Item 1, Item 2, …)
function generateList(size) {
  return Array.from({ length: size }, (_, index) => `Item ${index + 1}`);
}

export default function VirtualizedList() {
  // State to trigger a re-render (for demo purposes)
  const [triggerRerender, setTriggerRerender] = useState(false);
  // State to hold the generated list of items
  const [items, setItems] = useState([]);

  // Handler: generates a list of 100000 items, then forces a re-render after 1s
  const handleGenerateList = () => {
    setItems(generateList(100000));

    setTimeout(() => {
      setTriggerRerender((prev) => !prev); // toggles boolean, re-renders component
    }, 1000);
  };

  // Row renderer: tells List how to render each row
  // `style` is required for proper positioning
  const rowRenderer = useCallback(
    ({ key, index, style }) => (
      <div key={key} style={style}>
        {items[index]}
      </div>
    ),
    [items]
  );

  return (
    <div>
      {/* Button to generate the list */}
      <button onClick={handleGenerateList}>Generate list</button>

      {/* Container for virtualized list with fixed height */}
      <div style={{ width: "100%", height: "400px" }}>
        <AutoSizer>
          {/* AutoSizer passes available width/height to List */}
          {({ width, height }) => (
            <List
              width={width}             // dynamic width
              height={height}           // dynamic height
              rowCount={items.length}   // total number of rows
              rowHeight={20}            // fixed height per row
              rowRenderer={rowRenderer} // render function for each row
            />
          )}
        </AutoSizer>
      </div>
    </div>
  );
}


Here’s a CodeSandBox Preview of React-Virtualized.

CAVEAT: Virtualization can affect accessibility. (screen readers may not read hidden items).

At the end of the day, users don’t care about bundles, renders, or virtualization; they care about apps that feel instant. By trimming bundles, avoiding wasted renders, and rendering only what’s needed, you deliver speed without compromise.

I’d love to hear your thoughts, too. If you have questions about these techniques—or if you’ve run into performance challenges in your own React projects—drop them in the comments. I wrote this piece because I’ve seen how frustrating sluggish apps can be, and I know these strategies can make a real difference. Your feedback and questions can spark even more discussion and help others who are facing the same issues.


This content originally appeared on DEV Community and was authored by Fenigma