Skip
Arish's avatar

27. Code Splitting and Lazy Loading


Code Splitting

Code splitting lets you split your bundle into smaller chunks that load on demand.

React.lazy

Load components only when needed:

jsx
1import { lazy, Suspense } from 'react'
2
3// Instead of static import
4// import Dashboard from './Dashboard'
5
6// Use lazy loading
7const Dashboard = lazy(() => import('./Dashboard'))
8const Settings = lazy(() => import('./Settings'))
9const Profile = lazy(() => import('./Profile'))
10
11function App() {
12  return (
13    <Suspense fallback={<LoadingSpinner />}>
14      <Routes>
15        <Route path="/dashboard" element={<Dashboard />} />
16        <Route path="/settings" element={<Settings />} />
17        <Route path="/profile" element={<Profile />} />
18      </Routes>
19    </Suspense>
20  )
21}

Suspense Boundaries

Control loading states with Suspense:

jsx
1function App() {
2  return (
3    <div>
4      {/* Global fallback */}
5      <Suspense fallback={<FullPageLoader />}>
6        <Header />
7        
8        {/* Section-specific fallback */}
9        <Suspense fallback={<SidebarSkeleton />}>
10          <Sidebar />
11        </Suspense>
12        
13        <main>
14          <Suspense fallback={<ContentSkeleton />}>
15            <MainContent />
16          </Suspense>
17        </main>
18      </Suspense>
19    </div>
20  )
21}

Route-Based Splitting

The most common pattern:

jsx
1import { lazy, Suspense } from 'react'
2import { Routes, Route } from 'react-router-dom'
3
4// Lazy load all route components
5const Home = lazy(() => import('./pages/Home'))
6const Products = lazy(() => import('./pages/Products'))
7const ProductDetail = lazy(() => import('./pages/ProductDetail'))
8const Cart = lazy(() => import('./pages/Cart'))
9const Checkout = lazy(() => import('./pages/Checkout'))
10const Admin = lazy(() => import('./pages/Admin'))
11
12function App() {
13  return (
14    <Suspense fallback={<PageLoader />}>
15      <Routes>
16        <Route path="/" element={<Home />} />
17        <Route path="/products" element={<Products />} />
18        <Route path="/products/:id" element={<ProductDetail />} />
19        <Route path="/cart" element={<Cart />} />
20        <Route path="/checkout" element={<Checkout />} />
21        <Route path="/admin/*" element={<Admin />} />
22      </Routes>
23    </Suspense>
24  )
25}
26
27function PageLoader() {
28  return (
29    <div className="flex items-center justify-center h-screen">
30      <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500" />
31    </div>
32  )
33}

Named Exports

For components with named exports:

jsx
1// Component with named export
2export function Dashboard() { ... }
3
4// Lazy load with named export
5const Dashboard = lazy(() =>
6  import('./Dashboard').then(module => ({
7    default: module.Dashboard
8  }))
9)

Preloading Components

Load before user needs them:

jsx
1const Dashboard = lazy(() => import('./Dashboard'))
2
3// Preload on hover
4function NavLink() {
5  const preload = () => {
6    import('./Dashboard')
7  }
8  
9  return (
10    <Link to="/dashboard" onMouseEnter={preload}>
11      Dashboard
12    </Link>
13  )
14}
15
16// Preload after initial render
17useEffect(() => {
18  const timer = setTimeout(() => {
19    import('./Dashboard')
20    import('./Settings')
21  }, 2000)
22  
23  return () => clearTimeout(timer)
24}, [])

Heavy Libraries

Split large dependencies:

jsx
1// ❌ Loads chart library with initial bundle
2import { Chart } from 'chart.js'
3
4// ✅ Load only when needed
5const ChartComponent = lazy(() => import('./ChartComponent'))
6
7function Dashboard() {
8  const [showChart, setShowChart] = useState(false)
9  
10  return (
11    <div>
12      <button onClick={() => setShowChart(true)}>Show Chart</button>
13      
14      {showChart && (
15        <Suspense fallback={<ChartSkeleton />}>
16          <ChartComponent />
17        </Suspense>
18      )}
19    </div>
20  )
21}
jsx
1const Modal = lazy(() => import('./Modal'))
2
3function App() {
4  const [showModal, setShowModal] = useState(false)
5  
6  return (
7    <div>
8      <button onClick={() => setShowModal(true)}>Open Modal</button>
9      
10      {showModal && (
11        <Suspense fallback={null}>
12          <Modal onClose={() => setShowModal(false)} />
13        </Suspense>
14      )}
15    </div>
16  )
17}

Error Boundaries

Handle loading failures:

jsx
1class ErrorBoundary extends React.Component {
2  state = { hasError: false }
3  
4  static getDerivedStateFromError(error) {
5    return { hasError: true }
6  }
7  
8  render() {
9    if (this.state.hasError) {
10      return (
11        <div>
12          <h2>Failed to load component</h2>
13          <button onClick={() => window.location.reload()}>
14            Reload page
15          </button>
16        </div>
17      )
18    }
19    
20    return this.props.children
21  }
22}
23
24// Usage
25<ErrorBoundary>
26  <Suspense fallback={<Loading />}>
27    <LazyComponent />
28  </Suspense>
29</ErrorBoundary>

Bundle Analysis

Analyze your bundle size:

bash
1npm install -D source-map-explorer
2
3# Add to package.json scripts
4"analyze": "source-map-explorer 'build/static/js/*.js'"

Vite Dynamic Imports

Vite automatically code-splits on dynamic imports:

jsx
1// Vite splits this into a separate chunk
2const Component = lazy(() => import('./Component'))
3
4// Named chunks for better debugging
5const Dashboard = lazy(() =>
6  import(/* webpackChunkName: "dashboard" */ './Dashboard')
7)

Code splitting dramatically improves initial load time!