Code Splitting and Lazy Loading in React: Performance Optimization Guide | SoniNow Blog

Limited TimeLearn More

reactcode splittinglazy loadingperformanceweb development

Code Splitting and Lazy Loading in React: Performance Optimization Guide

Published

2026-06-23

Read Time

4 mins

Code Splitting and Lazy Loading in React: Performance Optimization Guide

Shipping a single JavaScript bundle to every visitor is the fastest way to degrade your application performance. Code splitting divides your bundle into smaller chunks that load on demand. Lazy loading defers the loading of components until they are actually needed. Together, they are the most impactful performance optimization for React applications.

Route-Based Code Splitting

The easiest and most effective splitting strategy is by route. Every route in your application should load only the JavaScript needed for that page.

import { lazy, Suspense } from 'react'
import { LoadingSpinner } from '@/components/ui/loading-spinner'

// Each route loads its own chunk
const DashboardPage = lazy(() => import('@/pages/dashboard'))
const SettingsPage = lazy(() => import('@/pages/settings'))
const AnalyticsPage = lazy(() => import('@/pages/analytics'))

export function AppRouter() {
  return (
    <Routes>
      <Route
        path="/dashboard"
        element={
          <Suspense fallback={<LoadingSpinner />}>
            <DashboardPage />
          </Suspense>
        }
      />
      <Route
        path="/settings"
        element={
          <Suspense fallback={<LoadingSpinner />}>
            <SettingsPage />
          </Suspense>
        }
      />
      <Route
        path="/analytics"
        element={
          <Suspense fallback={<LoadingSpinner />}>
            <AnalyticsPage />
          </Suspense>
        }
      />
    </Routes>
  )
}

With Next.js App Router, route-based splitting happens automatically — each page.tsx is its own chunk. The benefit is built-in and zero-config. For custom React apps using React Router or TanStack Router, configure lazy loading explicitly.

Component-Level Lazy Loading

Components that are not visible on initial render — modals, side panels, heavy data visualizations — should lazy load. The pattern is the same but at component granularity.

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

const DataChart = lazy(() => import('@/components/data-chart'))
const ExportModal = lazy(() => import('@/components/export-modal'))

export function AnalyticsDashboard() {
  const [showExport, setShowExport] = useState(false)

  return (
    <div>
      <Suspense fallback={<ChartSkeleton />}>
        <DataChart />
      </Suspense>

      <button onClick={() => setShowExport(true)}>Export</button>

      {showExport && (
        <Suspense fallback={null}>
          <ExportModal onClose={() => setShowExport(false)} />
        </Suspense>
      )}
    </div>
  )
}

The DataChart component, which might bundle a charting library like D3 or Recharts, does not load until the parent component renders. The ExportModal only loads when the user clicks the export button. This reduces initial bundle size by hundreds of kilobytes.

Named Exports and Dynamic Imports

React.lazy only works with default exports. If your component uses a named export, re-export it as default:

// components/data-chart.ts
export function DataChart(props: DataChartProps) {
  return <ChartComponent {...props} />
}

// Re-export as default for lazy loading
export { DataChart as default }

For utilities and non-component code, use dynamic import() directly:

async function handleExport() {
  // Dynamic import — only loads when this function is called
  const { generateCSV } = await import('@/lib/export')
  const { formatDate } = await import('@/lib/format')

  const csv = generateCSV(data.map((d) => ({
    ...d,
    date: formatDate(d.date),
  })))

  downloadFile(csv, 'export.csv')
}

This pattern is especially useful for heavy utility libraries like date formatters, markdown parsers, or image processing functions that are only needed in specific user flows.

Preloading for Instant Transitions

Lazy loading reduces initial load but can create a delay when the user triggers a lazy-loaded component. Use React.lazy's preloading capabilities or manual preload hints:

import { lazy } from 'react'

const ProductGallery = lazy(() => import('@/components/product-gallery'))

// Preload on hover
function ProductCard() {
  const handleMouseEnter = () => {
    const preloadPromise = import('@/components/product-gallery')
  }

  return (
    <div onMouseEnter={handleMouseEnter}>
      <Link to="/products/1">View Product</Link>
    </div>
  )
}

When the user hovers over a link, start loading the chunk. By the time they click, the component is likely already cached. The difference between a 200ms load and an instant render is the difference between a user staying and leaving.

Measuring the Impact

Use the Coverage tab in Chrome DevTools to identify code that loads but is not used on initial render. Set a target: reduce initial JavaScript by 50% through route-based splitting. Then iterate into component-level splitting for the highest-value targets.

Code splitting and lazy loading are fundamental to performance but require careful execution to avoid layout shifts and loading jank. At SoniNow, we optimize React applications for fast initial loads while maintaining smooth user experiences — measuring real-world impact with Lighthouse and Web Vitals.

Want faster load times for your React app? Talk to SoniNow about our performance optimization services.