Lesson 63 of 80
React.lazy splits your bundle at the component level — the real skill is deciding where to cut, not the one-liner that makes it work.
14 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
The mechanics of React.lazy and Suspense — a dynamic import() that suspends until it resolves, caught by a fallback — are a single, small trick. The actual engineering problem is where in a real app you put the cuts. Split too little and your initial bundle still ships code nobody needed yet; split too aggressively and you turn one network request into a chain of them, each waiting on the last.
The most common, highest-leverage place to cut is the route boundary. A user visiting /dashboard has no reason to download the code for /settings, /admin, or a rarely-opened export modal — none of that code should be in the bundle their browser parses before they can see anything.
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 AdminPanel = lazy(() => import("./pages/AdminPanel"));
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/admin" element={<AdminPanel />} />
</Routes>
</Suspense>
);
}
Now the bundler emits a separate chunk per route. Visiting / never triggers a request for AdminPanel's code at all — it simply isn't part of what the browser had to download to render the first page.
The next layer of the skill is preloading on intent, so the split doesn't cost a visible delay when the user does navigate:
function NavLink({ to, preload, children }) {
return (
<a
href={to}
onMouseEnter={preload} // kick off the import() before the click lands
onFocus={preload}
>
{children}
</a>
);
}
// preload={() => import("./pages/Settings")}
Hovering a link starts the same import() React.lazy would eventually trigger — by the time the click registers, the chunk is often already in flight or cached, so the Suspense fallback never even has time to show.
Dashboard itself lazily imports Widget, the browser can't start fetching Widget's chunk until Dashboard's chunk has downloaded and run far enough to reach that import() call — each hop adds a full round trip. Flattening imports at the same level lets a bundler request them in parallel instead.lazy(() => import("./EverythingBelowHere")) barely helps initial load if that's still 80% of the app's code; a dozen tiny lazy components each guarded by their own Suspense boundary can produce a dozen visible loading flickers for content that was going to render together anyway. Split at real user-facing boundaries — routes, modals, rarely-used panels — not arbitrarily.The React.lazy + Suspense mechanism itself — the dynamic import that suspends, and the boundary that catches it — is the fundamentals lesson earlier in this track. What's here is the applied layer on top of it: treating code-splitting as a deliberate architecture decision (route boundaries, preload-on-intent, avoiding waterfalls) rather than a single working example. It's also a narrower cousin of Suspense used for a different purpose elsewhere in this track — waiting on data instead of waiting on code — same boundary component, different thing being awaited underneath it.