How to code split in React

Code splitting in React reduces initial bundle size by loading components on-demand, improving application performance and load times. With over 12 years of React experience since 2014 and as the creator of CoreUI, I’ve implemented code splitting in numerous production applications. React’s React.lazy and Suspense enable component-level code splitting with dynamic imports creating separate bundles loaded when needed. This approach significantly reduces initial JavaScript payload while maintaining smooth user experience with lazy-loaded features.

Use React.lazy with Suspense and dynamic imports for route-based and component-level code splitting.

Basic React.lazy usage:

// App.jsx
import { lazy, Suspense } from 'react'

// Regular import - included in main bundle
import Header from './Header'

// Lazy import - separate bundle
const Dashboard = lazy(() => import('./Dashboard'))
const Settings = lazy(() => import('./Settings'))

export default function App() {
  return (
    <div>
      <Header />
      <Suspense fallback={<div>Loading...</div>}>
        <Dashboard />
      </Suspense>
    </div>
  )
}

Route-based code splitting:

// App.jsx
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'

// Always loaded
import Layout from './Layout'

// Lazy-loaded routes
const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Profile = lazy(() => import('./pages/Profile'))
const Settings = lazy(() => import('./pages/Settings'))

function App() {
  return (
    <BrowserRouter>
      <Layout>
        <Suspense fallback={<div className="loading">Loading...</div>}>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
            <Route path="/dashboard" element={<Dashboard />} />
            <Route path="/profile" element={<Profile />} />
            <Route path="/settings" element={<Settings />} />
          </Routes>
        </Suspense>
      </Layout>
    </BrowserRouter>
  )
}

export default App

Loading component:

// LoadingSpinner.jsx
export default function LoadingSpinner() {
  return (
    <div className="loading-container">
      <div className="spinner"></div>
      <p>Loading...</p>
    </div>
  )
}

// CSS
.loading-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: 200px;
}

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #3498db;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

// Usage
<Suspense fallback={<LoadingSpinner />}>
  <LazyComponent />
</Suspense>

Named exports with lazy:

// components/Charts.jsx
export function LineChart() {
  return <div>Line Chart</div>
}

export function BarChart() {
  return <div>Bar Chart</div>
}

// App.jsx
import { lazy } from 'react'

// Lazy load specific named export
const LineChart = lazy(() =>
  import('./components/Charts').then(module => ({
    default: module.LineChart
  }))
)

const BarChart = lazy(() =>
  import('./components/Charts').then(module => ({
    default: module.BarChart
  }))
)

Error boundaries with lazy:

// ErrorBoundary.jsx
import { Component } from 'react'

class ErrorBoundary extends Component {
  constructor(props) {
    super(props)
    this.state = { hasError: false, error: null }
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error }
  }

  componentDidCatch(error, errorInfo) {
    console.error('Code splitting error:', error, errorInfo)
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="error">
          <h2>Failed to load component</h2>
          <button onClick={() => window.location.reload()}>
            Reload Page
          </button>
        </div>
      )
    }

    return this.props.children
  }
}

export default ErrorBoundary

// Usage
import ErrorBoundary from './ErrorBoundary'

function App() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<LoadingSpinner />}>
        <LazyComponent />
      </Suspense>
    </ErrorBoundary>
  )
}

Preloading components:

// LazyComponent.jsx
import { lazy } from 'react'

const HeavyComponent = lazy(() => import('./HeavyComponent'))

// Preload function
HeavyComponent.preload = () => import('./HeavyComponent')

export default function App() {
  return (
    <div>
      {/* Preload on hover */}
      <button
        onMouseEnter={() => HeavyComponent.preload()}
        onClick={() => setShow(true)}
      >
        Show Heavy Component
      </button>

      {show && (
        <Suspense fallback={<div>Loading...</div>}>
          <HeavyComponent />
        </Suspense>
      )}
    </div>
  )
}

Webpack magic comments:

// Control chunk naming and loading behavior
const Dashboard = lazy(() =>
  import(
    /* webpackChunkName: "dashboard" */
    /* webpackPrefetch: true */
    './Dashboard'
  )
)

const Settings = lazy(() =>
  import(
    /* webpackChunkName: "settings" */
    /* webpackPreload: true */
    './Settings'
  )
)

const Reports = lazy(() =>
  import(
    /* webpackChunkName: "reports" */
    './Reports'
  )
)

Library code splitting:

// Heavy libraries can be split too
import { lazy } from 'react'

// Split chart library
const ChartComponent = lazy(() =>
  Promise.all([
    import('chart.js'),
    import('./ChartWrapper')
  ]).then(([chartjs, ChartWrapper]) => ({
    default: ChartWrapper.default
  }))
)

// Split markdown editor
const MarkdownEditor = lazy(() =>
  import('react-markdown-editor-lite').then(module => ({
    default: module.default
  }))
)

Component-based splitting:

// SplitComponent.jsx
import { lazy, Suspense, useState } from 'react'

const HeavyFeature = lazy(() => import('./HeavyFeature'))

export default function SplitComponent() {
  const [showFeature, setShowFeature] = useState(false)

  return (
    <div>
      <h1>Main Content</h1>
      <p>This loads immediately</p>

      <button onClick={() => setShowFeature(true)}>
        Load Heavy Feature
      </button>

      {showFeature && (
        <Suspense fallback={<div>Loading feature...</div>}>
          <HeavyFeature />
        </Suspense>
      )}
    </div>
  )
}

Vite code splitting:

// vite.config.js
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // Vendor chunks
          react: ['react', 'react-dom'],
          router: ['react-router-dom'],
          // Feature chunks
          charts: ['chart.js', 'react-chartjs-2'],
          forms: ['formik', 'yup']
        }
      }
    },
    chunkSizeWarningLimit: 500
  }
})

// Components automatically code-split with lazy()
import { lazy } from 'react'
const Dashboard = lazy(() => import('./Dashboard'))

Multiple Suspense boundaries:

// App.jsx
import { lazy, Suspense } from 'react'

const Sidebar = lazy(() => import('./Sidebar'))
const Content = lazy(() => import('./Content'))
const Footer = lazy(() => import('./Footer'))

export default function App() {
  return (
    <div className="app">
      {/* Independent loading states */}
      <Suspense fallback={<div>Loading sidebar...</div>}>
        <Sidebar />
      </Suspense>

      <Suspense fallback={<div>Loading content...</div>}>
        <Content />
      </Suspense>

      <Suspense fallback={<div>Loading footer...</div>}>
        <Footer />
      </Suspense>
    </div>
  )
}

Analyze bundle size:

# Install bundle analyzer
npm install --save-dev webpack-bundle-analyzer

# For Create React App
npm install --save-dev cra-bundle-analyzer
npx cra-bundle-analyzer

# For Vite
npm install --save-dev rollup-plugin-visualizer
# Then check dist/stats.html after build

Testing lazy components:

// LazyComponent.test.jsx
import { render, screen, waitFor } from '@testing-library/react'
import { Suspense } from 'react'
import LazyComponent from './LazyComponent'

test('renders lazy component', async () => {
  render(
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  )

  // Initially shows fallback
  expect(screen.getByText('Loading...')).toBeInTheDocument()

  // Wait for lazy component to load
  await waitFor(() => {
    expect(screen.getByText('Lazy Content')).toBeInTheDocument()
  })
})

Best Practice Note

Use route-based code splitting to split by page. Wrap lazy imports with Suspense and fallback UI. Use ErrorBoundary to handle loading failures gracefully. Preload components on user interaction like hover. Use webpack magic comments for chunk naming and prefetch. Split heavy third-party libraries into separate chunks. Keep critical path code in main bundle. Analyze bundle with webpack-bundle-analyzer. This is how we implement code splitting in CoreUI React applications—route-based splitting with React.lazy, error boundaries, and strategic preloading reducing initial bundle by 60-70% while maintaining fast navigation.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author

Subscribe to our newsletter
Get early information about new products, product updates and blog posts.
How to declare the optional function parameters in JavaScript?
How to declare the optional function parameters in JavaScript?

How to round a number to two decimal places in JavaScript
How to round a number to two decimal places in JavaScript

How to check if an array is empty in JavaScript?
How to check if an array is empty in JavaScript?

What is JavaScript Array.pop() Method?
What is JavaScript Array.pop() Method?

Answers by CoreUI Core Team