Lesson 38 of 81
Not every component needs to be in the first JavaScript bundle the browser downloads. next/dynamic lets you defer a Client Component's code until the moment it's actually needed — or skip server rendering for it entirely.
12 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
next/dynamic is Next.js's wrapper around React's lazy() plus a <Suspense> boundary, packaged as one function: dynamic(() => import('./Component')). Instead of bundling every Client Component's code into the page's initial JavaScript payload, a dynamic import splits that component into its own chunk and only downloads it when the component is actually about to render.
Server Components already get this for free — Next.js code-splits them automatically. next/dynamic exists for Client Components: things like a rich text editor, a charting library, a modal that's hidden until a button click, or any third-party widget that's heavy but not needed on first paint. The browser downloads less JavaScript up front, which means less to parse and execute before the page becomes interactive.
The second thing next/dynamic gives you, on top of code-splitting, is the ssr: false option — a way to tell Next.js "don't even try to render this on the server, only in the browser." That's the escape hatch for components that depend on browser-only globals (window, document, localStorage) and would throw during server rendering if Next.js attempted it.
// app/dashboard/usage-chart.tsx
"use client";
import dynamic from "next/dynamic";
// This heavy charting library reads `window` at import time,
// so it can only run in the browser.
const LineChart = dynamic(() => import("./line-chart"), {
ssr: false,
loading: () => <p>Loading chart...</p>,
});
export function UsageChart({ data }: { data: number[] }) {
return (
<section>
<h2>Usage this month</h2>
<LineChart points={data} />
</section>
);
}
// app/dashboard/page.tsx
import dynamic from "next/dynamic";
// Defer the whole edit modal's code until the user actually opens it.
const EditProfileModal = dynamic(() => import("./edit-profile-modal"));
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<EditProfileModal />
</div>
);
}
LineChart never ships in the page's initial bundle — its code downloads only once UsageChart renders, and ssr: false means Next.js doesn't waste a server render attempt on a component that would crash without window. EditProfileModal is a plainer case: it's still prerendered on the server by default (dynamic imports keep SSR unless you opt out), but its code is still a separate chunk, which matters once a page has a dozen rarely-used pieces like this adding up.
ssr: false is opt-in, not automatic — most components you wrap in dynamic() are there purely for code-splitting and keep their normal server-rendered HTML.dynamic() has to be called at the top level of a module, not inside a render function or an event handler. Next.js needs to statically match the import() call to bundle IDs it can preload; calling dynamic() conditionally or inside a component body breaks that matching.dynamic(() => import(...)) must be a literal string, not a variable or template string. The bundler resolves this at build time, and a computed path can't be traced back to a specific chunk.next/dynamic on everything is a net loss. Every dynamic import adds a network round trip and a loading state; it's a real win for genuinely heavy, rarely-needed code (a rich editor, a map library, a modal), not for a small component that was going to download in a few milliseconds anyway.next/dynamic defers a client bundle download in the browser; the App Router's <Suspense> streaming defers server-rendered HTML behind a slow data fetch. They can both appear in the same app for different reasons."Your dashboard's JavaScript bundle is bloated by a charting library that only 10% of visitors ever scroll far enough to see — what do you do?" tests whether a candidate reaches for next/dynamic specifically (not just "code splitting" as a vague answer), and can explain the ssr: false tradeoff correctly instead of applying it reflexively to every dynamic import.