Skip to content

Performance Optimization

Performance Optimization in React (Frontend Only)

Section titled “Performance Optimization in React (Frontend Only)”

When you build a React app for production, it generates:

  • HTML
  • CSS
  • JavaScript

Out of these, JavaScript is the biggest performance bottleneck.


Large JavaScript affects multiple stages:

  • Download time (network)
  • Parse time (browser reads JS)
  • Execution time (JS runs)
  • Main thread blocking (UI freezes)
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 at once:

  • Load only what is needed initially
  • Load remaining parts when required

This is called Code Splitting


  • Breaks one large JS bundle into smaller chunks
  • Loads chunks only when needed
graph LR A["App"] --> B["Core Chunk"] A --> C["Feature Chunk"] A --> D["Chart Chunk"] User1["Visit Home"] --> B User2["Visit Chart"] --> D


Used for heavy components (charts, editors, maps)

import { lazy, Suspense } from "react";
const HeavyChart = lazy(() => import("./HeavyChart"));
function Dashboard() {
return (
<Suspense fallback={<p>Loading chart...</p>}>
<HeavyChart />
</Suspense>
);
}
  • lazy() → dynamically imports component
  • Suspense → shows fallback while loading

Best optimization for real 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:

main.js (very large file with everything)

With splitting:

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

Browser loads only required chunks


Vite already splits code, but you can control grouping.

// vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
reactVendor: ["react", "react-dom", "react-router-dom"],
chartVendor: ["chart.js"],
},
},
},
},
};
  • Separate heavy libraries
  • Better caching
  • Avoid re-downloading unchanged code

  1. Split routes (biggest impact)
  2. Split heavy components (charts, editors)
  3. Optimize images (size + format)
  4. Remove unused dependencies
  5. Analyze bundle size
  6. Optimize rendering (only if needed)

Code Splitting

Load features only when needed.

Tree Shaking

Import only required functions from libraries.

Image Optimization

Use WebP/AVIF and responsive images.

Memoization

Prevent unnecessary re-renders.

Virtualization

Render only visible items in large lists.

Caching

Cache static assets effectively.


Tools:

  • React.memo
  • useMemo
  • useCallback

  • Importing full libraries for small usage
  • Loading heavy UI libraries on first load
  • Rendering large lists without virtualization
  • Over-optimizing too early
  • Ignoring bundle analysis

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

Use:

  • Chrome DevTools → Performance tab
  • Lighthouse
  • Bundle analyzers
  • First Contentful Paint (FCP)
  • Time to Interactive (TTI)
  • Main thread blocking time

Even if network is fast:

Performance = Network + CPU + Rendering
  • Most people optimize only network
  • Real bottleneck = CPU execution