Code Splitting

In a typical React application, tools like Webpack or Vite bundle all your code into a single large JavaScript file. As your app grows, this bundle becomes massive, leading to slow initial load times.

Code Splitting allows you to split your bundle into smaller chunks which can be loaded on demand.

[!TIP] Code splitting primarily improves Largest Contentful Paint (LCP) and Interaction to Next Paint (INP) by reducing the amount of JavaScript the browser needs to download and parse before the page becomes interactive.

1. Intuition: Packing for a Trip

Imagine you are going on a weekend beach trip.

  • Without Code Splitting: You pack your entire wardrobe—winter coats, ski boots, formal suits, and swimsuits—into one giant suitcase. It’s heavy, hard to carry, and you only use 5% of it.
  • With Code Splitting: You pack only what you need for the weekend. If you decide to go skiing later, you ask someone to ship your skis then.

In web development, shipping the “Settings Page” code to a user who just landed on the “Home Page” is like packing ski boots for the beach. It slows them down for no reason.

2. Hardware Reality: The Network Tax

Why does bundle size matter?

  1. Network Latency (RTT): On a fast 5G connection, downloading 5MB might take 1 second. On a flaky 3G connection (which many mobile users have), it could take 20 seconds.
  2. CPU Parse Time: Downloading is only half the battle. The browser must parse and compile the JavaScript. On a low-end Android device ($150 phone), parsing 1MB of JS can block the main thread for hundreds of milliseconds, making the UI unresponsive.

3. Bundle Architecture

Monolith vs. Split Chunks

Traditional Bundle
main.js
5.0 MB
Includes Home, Settings, Admin, Utils...
🐢 Slow Initial Load
Split Chunks
main
200 KB
Admin
1.5 MB
Lazy
Settings
1.2 MB
Lazy
🚀 Fast Initial Load

4. React.lazy and Suspense

React provides built-in support for code splitting with React.lazy and Suspense.

Syntax

// Before (Static Import)
import HeavyChart from './HeavyChart';

// After (Dynamic Import)
const HeavyChart = React.lazy(() => import('./HeavyChart'));

When you use React.lazy, the component is not loaded until it is first rendered. This means you must wrap it in a <span class="term-tooltip" tabindex="0" data-definition="A component that lets you 'wait' for some code to load and declaratively specify a loading state.">Suspense</span> boundary to handle the loading state.

import React, { Suspense } from 'react';

function MyPage() {
  return (
    <div>
      <Suspense fallback={<div>Loading chart...</div>}>
        <HeavyChart />
      </Suspense>
    </div>
  );
}

5. Interactive Demo: The Network Simulator

This demo simulates a slow network connection. Click “Load Feature” to trigger a lazy fetch. Observe how the UI enters a “Suspended” state (showing the fallback) until the “network request” completes.

Loading Interactive Demo...

6. Route-Based Splitting

The most common way to split code is by route. Users rarely need to load the “Settings” page code when they are on the “Home” page.

import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';

// 1. Dynamic Imports
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const Dashboard = lazy(() => import('./routes/Dashboard'));

const App = () => (
  <Router>
    {/* 2. Wrap Routes in Suspense */}
    <Suspense fallback={<div className="page-loader">Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/dashboard" element={<Dashboard />} />
      </Routes>
    </Suspense>
  </Router>
);

Route Transitions (React 18)

To prevent a jarring experience where the whole page turns white (or shows a spinner) during navigation, you can use React 18’s useTransition.

import { useTransition } from 'react';
import { useNavigate } from 'react-router-dom';

function Navigation() {
  const [isPending, startTransition] = useTransition();
  const navigate = useNavigate();

  function handleNavigation(url) {
    // 3. Mark the navigation as a transition
    startTransition(() => {
      navigate(url);
    });
  }

  return (
    <button
      onClick={() => handleNavigation('/profile')}
      disabled={isPending}
    >
      {isPending ? "Loading..." : "Go to Profile"}
    </button>
  );
}

This keeps the old UI visible while the new route’s code is fetching in the background.

7. Summary

Technique Description Best For
Route-based Split code by URL (Home vs Dashboard). Reducing initial bundle size significantly.
Component-based Lazy load heavy widgets (Modals, Charts, Maps). Optimizing interaction on complex pages.
Library-based Split large vendor libs (Moment.js, Lodash). Keeping main bundle lean.