Skip to content

Performance Optimization

When you build a React app for production, it creates three types of files:

  • HTML
  • CSS
  • JavaScript

Out of all three, JavaScript is the biggest reason your app slows down.


A large amount of JavaScript causes problems at many stages:

  • Download time (how long it takes over the network)
  • Parse time (how long the browser takes to read the JS)
  • Execution time (how long it takes for the JS to actually run)
  • Main thread blocking (this means the screen freezes up)
graph TD A["Large JS Bundle"] --> B["Slow Download"] A --> C["Slow Parse & Execute"] C --> D["Main Thread Blocked"] D --> E["Laggy UI / Delayed Interaction"]

Instead of loading everything in one go:

  • Load only what is needed right at the start
  • Load the rest later, only when it is actually needed

This idea is called Code Splitting.


  • It breaks one big JavaScript file into smaller pieces (called chunks)
  • It loads each chunk only when that chunk is actually needed
graph LR A["App"] --> B["Core Chunk"] A --> C["Feature Chunk"] A --> D["Chart Chunk"] User1["Visit Home"] --> B User2["Visit Chart"] --> D


This is used for heavy components (like charts, editors, or maps) that take a lot of space.

import { lazy, Suspense } from "react";
const HeavyChart = lazy(() => import("./HeavyChart"));
function Dashboard() {
return (
<Suspense fallback={<p>Loading chart...</p>}>
<HeavyChart />
</Suspense>
);
}
  • lazy() -> this loads the component only when it’s actually needed (this is called a dynamic import)
  • Suspense -> this shows a fallback message (like “Loading…”) while the component is being loaded

This gives the best results for real, full-sized apps.

import { lazy, Suspense } from "react";
import { Routes, Route } from "react-router-dom";
const Home = lazy(() => import("./pages/Home"));
const Settings = lazy(() => import("./pages/Settings"));
const Reports = lazy(() => import("./pages/Reports"));
export default function AppRoutes() {
return (
<Suspense fallback={<p>Loading page...</p>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/settings" element={<Settings />} />
<Route path="/reports" element={<Reports />} />
</Routes>
</Suspense>
);
}
graph LR A["User opens /"] --> B["Load Home Chunk"] C["User opens /reports"] --> D["Load Reports Chunk"] E["User opens /settings"] --> F["Load Settings Chunk"]

Without splitting, you get one giant file:

main.js (very large file with everything)

With splitting, you get several smaller files instead:

main.js
home.chunk.js
reports.chunk.js
settings.chunk.js

The browser only loads the chunks it actually needs


Vite already splits your code for you automatically, but you can control how it groups things.

// vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
reactVendor: ["react", "react-dom", "react-router-dom"],
chartVendor: ["chart.js"],
},
},
},
},
};
  • It keeps heavy libraries separate from the rest of your code
  • It improves caching (the browser remembers files better)
  • It avoids re-downloading code that hasn’t changed

  1. Split your routes first (this gives the biggest improvement)
  2. Split heavy components, like charts and editors
  3. Optimize your images (reduce size, use better formats)
  4. Remove any dependencies (libraries) you’re not using
  5. Check and analyze your bundle size
  6. Optimize how things render, but only if it’s actually needed

Code Splitting

Only load features when they are actually needed.

Tree Shaking

Only import the specific functions you need from a library, not the whole thing.

Image Optimization

Use formats like WebP or AVIF, and make images responsive (fit different screen sizes).

Memoization

Stop components from re-rendering when they don’t need to.

Virtualization

For long lists, only render the items the user can actually see on screen.

Caching

Store static files smartly so they don’t need to be downloaded again and again.


The tools you can use for this:

  • React.memo
  • useMemo
  • useCallback

  • Importing an entire library when you only need a small part of it
  • Loading heavy UI libraries right at the very first load
  • Showing long lists without using virtualization
  • Trying to over-optimize too early, before it’s actually a problem
  • Not bothering to check or analyze your bundle size

graph TD A["Initial Load"] --> B["Core App Loaded"] B --> C["User Navigates"] C --> D["Chunk Requested"] D --> E["Feature Rendered"]

You can use these tools:

  • Chrome DevTools -> Performance tab
  • Lighthouse
  • Bundle analyzers (tools that show what’s inside your bundle)
  • First Contentful Paint (FCP) - how fast the first bit of content shows up
  • Time to Interactive (TTI) - how fast the page becomes usable
  • Main thread blocking time - how long the screen stays frozen

Even if the network connection is fast, remember this:

Performance = Network + CPU + Rendering
  • Most people only try to fix the network part
  • But the real bottleneck is usually the CPU actually running the code