React.lazy & Dynamic import()
import() is a JavaScript language feature — not React-specific — that loads a module asynchronously at runtime and returns a Promise. React builds its lazy() API on top of it. Understanding both layers lets you use them beyond just splitting React components: loading polyfills on demand, feature-flagging heavy libraries, or bulk-importing files by pattern.
Dynamic import() Deep Dive
Static import statements run at parse time — they are hoisted to the top of the module and always evaluated. Dynamic import() is a function call that returns a Promise. The module is fetched and evaluated only when the import() expression is reached at runtime:
// Static import — loaded immediately when the bundle executes
import { format } from 'date-fns'
// Dynamic import — loaded on demand, returns a Promise
async function formatDate(date) {
// date-fns is only fetched the first time formatDate() is called
const { format } = await import('date-fns')
return format(date, 'yyyy-MM-dd')
}
// The Promise resolves to the module namespace object
import('./heavy-module').then(module => {
module.default() // access the default export
module.namedFn() // access a named export
})Loading Non-Component Modules Dynamically
import() shines for large libraries that are only needed in specific paths. Load them only when the user triggers the feature:
// ── Polyfill only when needed ──────────────────────────────────────
async function initSmoothScroll() {
if (!('scrollBehavior' in document.documentElement.style)) {
// Only load the polyfill on browsers that need it
await import('smoothscroll-polyfill').then(m => m.default.polyfill())
}
}
// ── Feature flag: only load analytics for opted-in users ────────────
async function initAnalytics(user) {
if (!user.analyticsEnabled) return
const { trackEvent } = await import('./analytics')
trackEvent('page_view', { userId: user.id })
}
// ── Heavy PDF generator — only when user clicks Export ──────────────
async function handleExport(data) {
const { generatePDF } = await import('./pdfGenerator') // ~180 KB
const blob = await generatePDF(data)
downloadBlob(blob, 'report.pdf')
}React.lazy Builds on import()
React.lazy is a thin wrapper that integrates import() with React's rendering cycle. It accepts a factory function that returns a Promise for a module with a default export that is a React component:
import { lazy, Suspense } from 'react'
// lazy() stores the Promise internally.
// On first render it throws the Promise (React catches it → shows Suspense fallback).
// When the Promise resolves, React re-renders with the real component.
const UserDashboard = lazy(() => import('./UserDashboard'))
// ── Named export? Create a re-export ────────────────────────────────
// In src/charts/LineChart.reexport.ts:
// export { LineChart as default } from './LineChart'
const LineChart = lazy(() => import('./charts/LineChart.reexport'))
// ── Or inline with .then() ───────────────────────────────────────────
const LineChart2 = lazy(() =>
import('./charts').then(module => ({ default: module.LineChart }))
)
function App() {
return (
<Suspense fallback={<p>Loading…</p>}>
<UserDashboard />
</Suspense>
)
}Preloading: Trigger import() Before the Click
Because import() returns a cached Promise on repeated calls, you can call it speculatively to warm the cache. The component renders immediately when the user actually clicks because the module is already downloaded:
import { lazy, Suspense, useState } from 'react'
const SettingsPanel = lazy(() => import('./SettingsPanel'))
// Calling this starts the download immediately — React.lazy will receive
// the same resolved Promise when SettingsPanel first renders
function preload() {
import('./SettingsPanel')
}
function Header() {
const [open, setOpen] = useState(false)
return (
<>
<button
onMouseEnter={preload} // start download on hover (~200ms head start)
onFocus={preload} // keyboard users too
onClick={() => setOpen(true)}
>
Open Settings
</button>
{open && (
<Suspense fallback={<p>Loading…</p>}>
<SettingsPanel onClose={() => setOpen(false)} />
</Suspense>
)}
</>
)
}Multiple Lazy Imports in Parallel
When several lazy components mount simultaneously, their fetches run in parallel. You don't need to do anything special — the browser handles concurrent requests. But you can also trigger parallel preloads programmatically:
import { lazy, Suspense } from 'react'
const ChartA = lazy(() => import('./ChartA'))
const ChartB = lazy(() => import('./ChartB'))
const ChartC = lazy(() => import('./ChartC'))
// All three chunks start downloading in parallel when Dashboard mounts.
// Each one shows its own fallback independently.
function Dashboard() {
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)' }}>
<Suspense fallback={<Skeleton />}><ChartA /></Suspense>
<Suspense fallback={<Skeleton />}><ChartB /></Suspense>
<Suspense fallback={<Skeleton />}><ChartC /></Suspense>
</div>
)
}
// Manual parallel preload — e.g. on route change start
function preloadDashboard() {
Promise.all([
import('./ChartA'),
import('./ChartB'),
import('./ChartC'),
])
}Error Handling with Retry
A chunk download can fail (flaky connection, CDN hiccup). Wrap the factory in a retry loop to recover automatically:
import { lazy } from 'react'
function lazyWithRetry(factory, retries = 3, delay = 500) {
return lazy(() =>
new Promise((resolve, reject) => {
const attempt = (remaining) => {
factory()
.then(resolve)
.catch((err) => {
if (remaining === 0) {
reject(err)
} else {
setTimeout(() => attempt(remaining - 1), delay)
}
})
}
attempt(retries)
})
)
}
const HeavyWidget = lazyWithRetry(() => import('./HeavyWidget'))Vite's import.meta.glob
Vite extends import() with import.meta.glob — a compile-time pattern that creates a map of lazy-import functions for all matching files. Useful for plugin systems, icon sets, or dynamic route loaders:
// import.meta.glob returns an object: { './icons/StarIcon.tsx': () => import(...) }
const icons = import.meta.glob('./icons/*.tsx')
async function loadIcon(name) {
const loader = icons[`./icons/${name}.tsx`]
if (!loader) throw new Error(`Icon not found: ${name}`)
const module = await loader()
return module.default
}
// Eager loading — imports all at build time (no lazy)
const allIcons = import.meta.glob('./icons/*.tsx', { eager: true })
// With React.lazy: auto-generate lazy components for all page files
const pages = import.meta.glob('./pages/*.tsx')
const routes = Object.entries(pages).map(([path, loader]) => ({
path: path.replace('./pages/', '/').replace('.tsx', ''),
Component: lazy(loader),
}))import()is a JavaScript language feature that returns a Promise for a module namespace object.React.lazywrapsimport()to integrate it with React rendering — the component suspends until the Promise resolves.Both only require the module to have a
defaultexport when used withReact.lazy.Repeated calls to
import()for the same path return the cached Promise — no extra requests.Preload by calling
import()on hover/focus before the user clicks.For bulk lazy loading by file pattern, use Vite's
import.meta.glob.