Code Splitting & Lazy Loading

Hard

Code splitting breaks your application bundle into smaller chunks loaded on demand, reducing the initial JavaScript download. React.lazy with dynamic import() creates lazy-loaded components, and Suspense provides the loading fallback. This is critical for large applications to maintain fast initial load times.

Interactive Visualization

Key Points

  • React.lazy accepts a function returning a dynamic import() promise
  • The bundler automatically creates a separate chunk for the lazy component
  • Suspense wraps lazy components and displays a fallback during loading
  • Route-level splitting is the highest-impact optimization
  • React.lazy only supports default exports; use re-export for named exports
  • Preloading chunks on hover or route prefetch improves perceived performance

Code Examples

Route-Level Code Splitting

import { lazy, Suspense } from 'react'

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

function LoadingSpinner() {
  return <div>Loading...</div>
}

function App({ currentRoute }: { currentRoute: string }) {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      {currentRoute === 'dashboard' && <Dashboard />}
      {currentRoute === 'settings' && <Settings />}
      {currentRoute === 'analytics' && <Analytics />}
    </Suspense>
  )
}

Each route component is a separate chunk that loads only when that route is visited, keeping the initial bundle small

Named Export and Preloading

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

// Lazy load named export by re-exporting as default
const Chart = lazy(() =>
  import('./Charts').then((mod) => ({ default: mod.PieChart }))
)

// Preload on hover for instant navigation
const preloadSettings = () => import('./Settings')

function Nav() {
  const [showChart, setShowChart] = useState(false)

  return (
    <div>
      <button
        onMouseEnter={preloadSettings}
        onClick={() => { /* navigate to settings */ }}
      >
        Settings
      </button>

      <button onClick={() => setShowChart(true)}>Show Chart</button>

      {showChart && (
        <Suspense fallback={<div>Loading chart...</div>}>
          <Chart />
        </Suspense>
      )}
    </div>
  )
}

Preloading on hover starts the download before the user clicks, making the transition feel instant

Common Mistakes

  • Not wrapping lazy components in a Suspense boundary causing a runtime error
  • Splitting too aggressively, creating many tiny chunks that hurt performance with HTTP overhead
  • Not handling chunk load failures with an error boundary

Interview Tips

  • Explain route-level vs component-level splitting and when each is appropriate
  • Know how to handle load failures with error boundaries for lazy components
  • Discuss prefetching strategies to improve perceived performance

Related Concepts