Code-Splitting & Lazy Loading Strategy

Ship a smaller first load by splitting at the right seams — routes and heavy, rarely-used features.

Code-splitting breaks your bundle into chunks that load on demand, so users download only the code the current view needs. lazy imports a component dynamically; Suspense shows a fallback while its chunk arrives. The skill is not the API — it is choosing where to split.

Where the seams are

  • Route boundaries — the highest-value split. Each page becomes its own chunk, so visiting /settings does not download the code for /dashboard.
  • Heavy, optional features — a rich text editor, a charting library, a map, a PDF viewer, an emoji picker. Load them when the user actually opens that feature, not on first paint.
  • Below-the-fold or modal content that most sessions never reach.
const Settings = lazy(() => import('./routes/Settings.jsx'));

<Suspense fallback={<PageSkeleton />}>
  <Settings />
</Suspense>

Make it feel fast, not janky

  • Skeletons over spinners. A layout-shaped placeholder feels quicker and avoids content-jump than a lone spinner.
  • Prefetch on intent. Kick off the dynamic import() when the user hovers a link or a route becomes likely — the chunk is often ready by the time they click.
  • Do not over-split. Dozens of tiny chunks add request overhead and can be slower than a few sensible ones. Split by route and by genuinely heavy dependencies, then stop.

Pair with an error boundary

A chunk can fail to load (flaky network, a deploy that removed the old file). Wrap lazy routes in an error boundary with a retry so a failed download is recoverable, not a dead end.

Example

Example · javascript
import { lazy, Suspense, useState } from 'react';

// Heavy editor split into its own chunk.
const RichEditor = lazy(() => import('./RichEditor.jsx'));

// Prefetch helper: warm the chunk before it is rendered.
function prefetchEditor() {
  import('./RichEditor.jsx');
}

function ComposeButton() {
  const [open, setOpen] = useState(false);
  return (
    <div>
      <button
        onMouseEnter={prefetchEditor} // start downloading on intent
        onFocus={prefetchEditor}
        onClick={() => setOpen(true)}
      >
        Write a post
      </button>
      {open && (
        <Suspense fallback={<p>Loading editor...</p>}>
          <RichEditor />
        </Suspense>
      )}
    </div>
  );
}

When to use it

  • A large SaaS app lazy-loads each route with React.lazy so first-time visitors download only the landing page bundle instead of the entire application.
  • A heavy chart library imported only on the analytics page is code-split so users who never visit analytics never download Recharts at all.
  • An admin panel behind an auth guard is lazy-loaded so its substantial codebase is excluded from the public bundle and only fetched after successful login.

More examples

Route-level lazy loading

Each route is a separate chunk; the browser only downloads Dashboard or Settings when the user navigates there, keeping the initial bundle small.

Example · js
import React, { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings  = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<PageSpinner />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings"  element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

Lazy-load a heavy component

Wrapping the editor in lazy and Suspense defers its bundle download until the component is actually rendered for the first time.

Example · js
const RichEditor = React.lazy(() => import('./RichEditor'));

function PostEditor({ visible }) {
  if (!visible) return null;
  return (
    <React.Suspense fallback={<div>Loading editor...</div>}>
      <RichEditor />
    </React.Suspense>
  );
}
// RichEditor's chunk is only fetched when visible becomes true

Prefetch chunk on link hover

Calling the same dynamic import on hover warms the browser cache so the lazy component mounts instantly when the user clicks, eliminating the loading state in practice.

Example · js
// Eagerly start the import on hover so the chunk is cached
// by the time the user clicks
const prefetchDashboard = () => import('./pages/Dashboard');

function NavLink() {
  return (
    <a
      href="/dashboard"
      onMouseEnter={prefetchDashboard}
      onFocus={prefetchDashboard}
    >
      Dashboard
    </a>
  );
}

Discussion

  • Be the first to comment on this lesson.