React.lazy And suspense — lazy components in React
React.lazy is a function that loads a React component dynamically, skipping it in the initial bundle until it actually renders. Pair it with Suspense to show a fallback during that load, and you get code splitting with almost no configuration.
Theory
TL;DR
React.lazy()wraps a dynamicimport()and returns a component React can render normallySuspensecatches the loading state and shows a fallback UI (spinner, skeleton, placeholder)- Webpack and Vite automatically create separate chunk files for lazy-loaded modules
- Only works with default exports - named exports need one extra step
- Best for route-level splits, heavy modals, and anything users rarely see on first load
Quick example
import React, { lazy, Suspense } from 'react';
// Vite/Webpack splits this into a separate chunk automatically
const Dashboard = lazy(() => import('./pages/Dashboard'));
function App() {
return (
<Suspense fallback={<p>Loading...</p>}>
<Dashboard />
</Suspense>
);
}When Dashboard renders for the first time, React pauses, fetches the chunk, then finishes rendering. The fallback prop is what users see in the meantime.
How the split happens
The bundler sees import('./pages/Dashboard') and creates a separate .js file for that module. React.lazy wraps the result in a Promise. When the component renders for the first time, React throws that Promise internally, Suspense catches it, renders the fallback, and re-renders once the Promise resolves.
You don't touch any of that machinery. The API is lazy + Suspense, the rest is automatic.
When to use
- Route-level splitting: every page behind a route is a good candidate. Users on
/homedon't need the/settingsbundle downloading immediately. - Heavy third-party UI: rich text editors, chart libraries, PDF viewers - lazy-load them.
- Modals and drawers: components that open on a button click load only when the user actually clicks.
- Admin sections: parts of the app most users never open.
Don't lazy-load tiny components or anything that appears immediately on every page render. The async overhead is real, even if small.
Named exports and lazy
React.lazy expects the dynamic import to resolve to a module with a default export. If your component uses a named export, wrap it like this:
// UserCard uses a named export
const UserCard = lazy(() =>
import('./UserCard').then((mod) => ({ default: mod.UserCard }))
);This is not a bug - it's just how the API is designed. I've seen this trip up teams who assumed lazy works with anything importable.
Common mistakes
Forgetting Suspense entirely:
// This throws at runtime
const Chart = lazy(() => import('./Chart'));
function Dashboard() {
return <Chart />; // No Suspense above - React will error
}Lazy components need a Suspense boundary somewhere above them in the tree. One boundary can cover multiple lazy components at once.
Defining lazy inside a component body:
// Bad: re-creates the lazy reference on every render
function Page() {
const Chart = lazy(() => import('./Chart')); // wrong
return <Chart />;
}Always define lazy components at the module level, outside any function.
No error boundary in production:
Network requests fail. If the chunk download errors, React crashes the whole tree unless you have an ErrorBoundary around the Suspense. In production, always pair them:
<ErrorBoundary fallback={<p>Failed to load.</p>}>
<Suspense fallback={<Spinner />}>
<LazyPage />
</Suspense>
</ErrorBoundary>Real-world usage
- React Router apps: lazy-load every route component, one
Suspenseat the router level - Next.js: uses
next/dynamic, which wraps the same pattern with extra options likessr: false - Component libraries: ship heavy components (DataGrid, RichEditor) as separate entry points so consumers can lazy-load them
Follow-up questions
Q: Can one Suspense boundary wrap multiple lazy components?
A: Yes. Suspense shows the fallback if any lazy child is still loading. Once all resolve, everything renders. This is useful for route-level splits with several lazy parts on one page.
Q: What happens when a lazy component's chunk fails to load?
A: React throws an error that propagates up the tree. Without an ErrorBoundary, the whole app unmounts. With one, only the boundary's subtree shows the error fallback.
Q: Does React.lazy work with Server Components in React 18+?
A: Not directly. On the server, dynamic imports work differently. Next.js handles this with next/dynamic and a { ssr: false } option. Pure React.lazy is client-only.
Q: What is the difference between React.lazy and next/dynamic?
A: next/dynamic wraps React.lazy and adds options: ssr: false to skip server rendering, and a built-in loading prop instead of requiring a Suspense wrapper. Same concept, different API layer.
Examples
Basic lazy component
import React, { lazy, Suspense } from 'react';
const HeavyChart = lazy(() => import('./HeavyChart'));
function ReportPage() {
return (
<div>
<h1>Monthly Report</h1>
<Suspense fallback={<div>Loading chart...</div>}>
<HeavyChart data={reportData} />
</Suspense>
</div>
);
}HeavyChart and its dependencies ship in a separate file. Users who never visit ReportPage never download that code.
Route-level code splitting with React Router
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import React, { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home'));
const Settings = lazy(() => import('./pages/Settings'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading page...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/settings" element={<Settings />} />
<Route path="/admin" element={<AdminPanel />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}One Suspense at the router level covers all routes. Each page loads only when the user navigates to it. The admin bundle never reaches a regular user unless they visit /admin.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.