RoadmapDay 36 / 80
React (Web)Month 2 · Week 8

Day 36: Web Performance: Code Splitting, Lazy Loading & Bundle Analysis

Reduce the JS a user must download before your app becomes interactive by finding and cutting what does not need to ship on day one.

Mark this day complete

Study

Concepts

Code splitting: ship less, on demand

A bundler (Webpack/Turbopack/Rollup) can split one large JS bundle into many smaller chunks, loaded on demand instead of all upfront. `React.lazy(() => import('./Modal'))` combined with `<Suspense>` is the standard React idiom: the Modal's code is not downloaded until the first time it is actually rendered, shrinking the INITIAL bundle a user must fetch and parse before the app becomes interactive.

Route-based splitting (one chunk per page) is the highest-leverage place to start — a user visiting the homepage should not download the checkout flow's code. Next.js and most modern routers do this automatically per route; for a plain SPA it is a manual `React.lazy` per route component.

Bundle analysis: find out what you are actually shipping

A bundle analyzer (e.g. `webpack-bundle-analyzer`, or Next.js's built-in `@next/bundle-analyzer`) renders a treemap of exactly what is inside your production bundle, sized by byte weight — this is how you discover a 300KB moment library imported for one date-format call, or a duplicated dependency pulled in by two different packages at two different versions.

Two metrics matter for the initial-load story: Total Blocking Time / Time To Interactive (how long until the page responds to input) is driven mostly by JS parse+execute time, so shipping less JS upfront directly improves it; Largest Contentful Paint is driven more by the biggest visible element (often an image) and critical CSS/font loading, which code splitting does not directly fix.

See It

Visualizations

Visualization

From one bundle to route-based chunks

Single bundle.js

everything, downloaded before ANY interaction

Split by route

home.chunk.js, checkout.chunk.js, ...

Split by interaction

Modal.chunk.js loads only when opened

Result: smaller initial payload

faster Time To Interactive

Build It

Code Examples

Lazy-loading a heavy component with Suspense

jsx
import { lazy, Suspense, useState } from 'react';

// Not downloaded until this module is actually imported at runtime
const ReportChart = lazy(() => import('./ReportChart'));

function Dashboard() {
  const [showReport, setShowReport] = useState(false);

  return (
    <>
      <button onClick={() => setShowReport(true)}>View Report</button>
      {showReport && (
        <Suspense fallback={<Spinner />}>
          <ReportChart /> {/* triggers the chunk download on first render */}
        </Suspense>
      )}
    </>
  );
}

Avoiding a heavy import for one small function

js
// Costly: pulls in the ENTIRE library (often 200KB+) for one function
import moment from 'moment';
const formatted = moment(date).format('YYYY-MM-DD');

// Better: a focused, tree-shakeable alternative, or a native API
const formatted = new Intl.DateTimeFormat('en-CA').format(date); // zero dependencies

// If you truly need a library, import only what you use from a
// modular one (date-fns) so bundlers can tree-shake the rest away
import { format } from 'date-fns';
const formatted2 = format(date, 'yyyy-MM-dd');

Remember

Key Takeaways

  • Code splitting defers loading JS until it is actually needed — route-based splitting is the highest-leverage starting point.
  • React.lazy + Suspense is the standard idiom for component-level splitting on interaction (modals, tabs, heavy widgets).
  • A bundle analyzer treemap is how you FIND bloat — never guess which dependency is heavy, measure it.
  • Time To Interactive is mostly about how much JS you ship and parse; Largest Contentful Paint is mostly about images/fonts/CSS.
  • Prefer native APIs or modular, tree-shakeable libraries over large all-in-one dependencies for small utility needs.

Do It

Practice

  1. 1Run a bundle analyzer on a real project and identify the single largest dependency by byte weight.
  2. 2Convert one route or modal in a project to React.lazy + Suspense and measure the initial bundle size before/after.
  3. 3Replace one moment.js (or similarly heavy) usage with a native Intl API call and confirm the bundle shrinks.