How to implement code splitting in JavaScript
Code splitting divides your JavaScript bundle into smaller chunks that load on demand, reducing initial page load time.
As the creator of CoreUI with over 25 years of JavaScript experience since 2000, I’ve implemented code splitting across large applications to dramatically improve first contentful paint times.
The standard approach uses dynamic import() to create split points, letting bundlers like webpack or Vite generate separate chunks automatically.
This allows users to download only the code they actually need.
Use dynamic import to split code at logical boundaries.
// Static import - bundled into main chunk
import { heavyFeature } from './heavy-feature'
// Dynamic import - creates a separate chunk
button.addEventListener('click', async () => {
const { heavyFeature } = await import('./heavy-feature')
heavyFeature()
})
Static imports always end up in the main bundle. Dynamic import() returns a Promise and tells the bundler to split here. The chunk downloads only when this code executes. The initial bundle stays small.
Route-Based Code Splitting
Load route components lazily in React.
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))
const Reports = lazy(() => import('./pages/Reports'))
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path='/' element={<Dashboard />} />
<Route path='/settings' element={<Settings />} />
<Route path='/reports' element={<Reports />} />
</Routes>
</Suspense>
)
}
React.lazy wraps dynamic imports for components. Suspense shows a fallback while loading. Each route becomes a separate chunk. Users downloading the dashboard don’t download reports code.
Component-Level Code Splitting
Split large components loaded conditionally.
import { lazy, Suspense, useState } from 'react'
const HeavyChart = lazy(() => import('./HeavyChart'))
const DataGrid = lazy(() => import('./DataGrid'))
function Dashboard() {
const [showChart, setShowChart] = useState(false)
const [showGrid, setShowGrid] = useState(false)
return (
<div>
<button onClick={() => setShowChart(true)}>Show Chart</button>
<button onClick={() => setShowGrid(true)}>Show Grid</button>
{showChart && (
<Suspense fallback={<div>Loading chart...</div>}>
<HeavyChart />
</Suspense>
)}
{showGrid && (
<Suspense fallback={<div>Loading grid...</div>}>
<DataGrid />
</Suspense>
)}
</div>
)
}
Heavy chart libraries only download when users click the button. Each component has its own loading state. This dramatically improves initial load for feature-rich dashboards.
Webpack Magic Comments
Control chunk naming and prefetching.
// Named chunk
const module = await import(
/* webpackChunkName: "analytics" */
'./analytics'
)
// Prefetch - download during idle time
const module = await import(
/* webpackPrefetch: true */
/* webpackChunkName: "settings" */
'./Settings'
)
// Preload - download with higher priority
const module = await import(
/* webpackPreload: true */
/* webpackChunkName: "critical" */
'./CriticalFeature'
)
Magic comments control bundler behavior. Named chunks produce readable filenames. Prefetch downloads chunks during browser idle time. Preload hints the browser to download with high priority alongside the parent chunk.
Measuring Bundle Impact
Verify code splitting is working.
# Analyze bundle with webpack
npx webpack --profile --json > stats.json
npx webpack-bundle-analyzer stats.json
# With Vite
npx vite build --mode production
# Check dist/ for multiple .js files
The bundle analyzer shows chunk sizes visually. Multiple small files instead of one large file confirms splitting works. Aim for an initial bundle under 200KB gzipped.
Best Practice Note
This is the same code splitting strategy we use in CoreUI to keep the library lightweight. Split at route boundaries first - it gives the biggest wins with the least effort. Then split large third-party libraries loaded conditionally. Avoid over-splitting: too many tiny chunks create excessive HTTP requests that cancel out the benefit. With HTTP/2, a few dozen chunks is fine. Profile with Lighthouse before and after to measure actual improvement.



