PerformanceReactMedium

How does code splitting work?

01

The Short Answer

Code splitting breaks your JavaScript bundle into smaller chunks that are loaded on demand rather than all at once. Instead of shipping one massive file containing your entire app, you split it at logical boundaries (routes, features, heavy components) so users only download the code they actually need for the current page. This dramatically reduces initial load time — especially for large applications where most users only visit a fraction of the available pages.

02

Why It Matters

Without code splitting, a bundler like webpack or Vite combines all your JavaScript into one or a few large files. A user visiting your homepage downloads code for the admin dashboard, settings page, and every other route — even if they never visit those pages. This wastes bandwidth, increases parse time, and delays interactivity.

📚

The encyclopedia analogy

Without code splitting, visiting a website is like being forced to carry an entire encyclopedia just to read one article. With code splitting, you only pick up the volume you need — and grab another one later if you need it.

03

Dynamic import() — The Mechanism

Code splitting is powered by dynamic import() — a function that loads a module asynchronously and returns a Promise. Unlike static import statements (which are bundled together), dynamic imports tell the bundler to create a separate chunk that's loaded at runtime only when the import is executed.

dynamic-import.tstypescript
// Static import — bundled into the main chunk (always loaded)
import { heavyFunction } from './heavy-module';

// Dynamic import — creates a separate chunk (loaded on demand)
async function handleClick() {
  const { heavyFunction } = await import('./heavy-module');
  heavyFunction();
}

// The bundler sees import() and creates:
// - main.js (your app code)
// - heavy-module.chunk.js (loaded only when handleClick runs)

The bundler automatically handles chunk creation, naming, and loading. You just use import() where you want a split point, and the tooling does the rest — generating separate files and inserting the loading logic.

04

Route-Based Splitting in React

The most impactful split point is at the route level — each page becomes its own chunk. React provides React.lazy() for this, which wraps a dynamic import and returns a component that loads its code on first render. Pair it with Suspense to show a fallback while the chunk loads.

route-splitting.tsxtypescript
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

// Each route is a separate chunk — loaded only when navigated to
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));

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

// Result: visiting / only loads home.chunk.js
// Navigating to /admin loads admin.chunk.js on demand

In Next.js, route-based splitting happens automatically — each page in the app/ or pages/ directory becomes its own chunk without any manual configuration. The framework handles lazy loading and prefetching for you.

05

Component-Level Splitting

Beyond routes, you can split heavy components that aren't needed immediately — modals, charts, rich text editors, or any feature that's triggered by user interaction. This keeps the initial bundle lean and loads expensive code only when the user actually needs it.

component-splitting.tsxtypescript
import { lazy, Suspense, useState } from 'react';

// Heavy chart library — only loaded when user opens analytics
const AnalyticsChart = lazy(() => import('./components/AnalyticsChart'));

// Heavy markdown editor — only loaded when user clicks "Edit"
const MarkdownEditor = lazy(() => import('./components/MarkdownEditor'));

function ArticlePage({ article }: { article: Article }) {
  const [editing, setEditing] = useState(false);
  const [showAnalytics, setShowAnalytics] = useState(false);

  return (
    <div>
      <h1>{article.title}</h1>

      {editing ? (
        <Suspense fallback={<Spinner />}>
          <MarkdownEditor content={article.body} />
        </Suspense>
      ) : (
        <article>{article.body}</article>
      )}

      <button onClick={() => setShowAnalytics(true)}>View Analytics</button>

      {showAnalytics && (
        <Suspense fallback={<ChartSkeleton />}>
          <AnalyticsChart articleId={article.id} />
        </Suspense>
      )}
    </div>
  );
}
06

Prefetching and Preloading

Code splitting introduces a tradeoff: smaller initial load, but a delay when navigating to a new route or opening a lazy component. Prefetching mitigates this by loading chunks in the background before the user needs them — on hover, on idle, or when a link enters the viewport.

prefetching.tsxtypescript
// Prefetch on hover — load the chunk before the user clicks
const AdminPanel = lazy(() => import('./pages/AdminPanel'));

function NavLink() {
  const prefetchAdmin = () => {
    // Triggers the chunk download without rendering
    import('./pages/AdminPanel');
  };

  return (
    <a
      href="/admin"
      onMouseEnter={prefetchAdmin}
      onFocus={prefetchAdmin}
    >
      Admin
    </a>
  );
}

// Prefetch on idle — load non-critical chunks when browser is idle
if ('requestIdleCallback' in window) {
  requestIdleCallback(() => {
    import('./pages/Settings');
    import('./pages/AdminPanel');
  });
}

Next.js prefetches automatically

Next.js prefetches linked routes when <Link> components enter the viewport. You get the benefits of code splitting without the navigation delay — chunks are already loaded by the time the user clicks.

07

What to Split

Good candidates for splitting

  • Routes / pages (biggest impact, easiest to implement)
  • Heavy third-party libraries (chart libs, editors, PDF viewers)
  • Features behind user interaction (modals, drawers, tabs)
  • Admin-only features (most users never see them)
  • Below-the-fold content (not visible on initial viewport)

Don't split

  • Tiny components (overhead of a network request outweighs the savings)
  • Components needed for initial render (adds delay to first paint)
  • Shared utilities used everywhere (they'd be loaded immediately anyway)
  • Critical-path UI (header, navigation, layout shell)
08

Why Interviewers Ask This

This question tests your understanding of frontend performance optimization and build tooling. Interviewers want to see that you know how dynamic import() creates split points, can identify good candidates for splitting, understand the tradeoff (smaller initial load vs navigation delay), know how to mitigate that tradeoff with prefetching, and have practical experience with React.lazy/Suspense or framework-level splitting. It shows you think about user experience beyond just writing correct code.

Quick Revision Cheat Sheet

Mechanism: Dynamic import() tells bundler to create a separate chunk

React API: React.lazy(() => import('./Component')) + Suspense fallback

Best split point: Routes — each page as its own chunk (biggest impact)

Tradeoff: Smaller initial load, but delay on first navigation to new chunk

Mitigation: Prefetch on hover/idle/viewport entry

Next.js: Automatic per-page splitting + Link prefetching built-in