In a Next.js App Router app, what does the `ssr: false` option on `next/dynamic` change about where a component renders, and why does the build reject that option inside a Server Component?
answer
- two things are being switched off, not one
- think about which environments have a DOM
- the fallback is what the HTML actually contains
- a Server Component has no second render pass
- must live behind 'use client'
basics
~20 sWith ssr: false, Next.js skips the component during server rendering and mounts it only in the browser after hydration, rendering the loading fallback in its place meanwhile. The App Router rejects the option in a Server Component because opting out of server rendering is a client-side decision.
solid answer
~60 s`ssr: false` tells `next/dynamic` not to include the component in the server-rendered HTML at all. The server output carries your `loading` fallback in that slot — or nothing, if you did not supply one — and the real component's chunk is fetched and mounted on the client after hydration. You use it for code that cannot run on the server: a library that touches `window` or `document` at import time, a canvas or WebGL widget, anything reading browser-only APIs during render. In the App Router as of Next 15 and 16, the option is only legal in a file marked `'use client'`; putting it in a Server Component fails the build with an error telling you to move it into a Client Component. That makes sense — a Server Component has no client-side render pass to defer to, so "skip the server render" is meaningless there. The cost is real: that region is empty in the initial HTML, so it contributes nothing to first paint and can shift layout when it arrives.
code
tsx · 17 lines'use client'
import dynamic from 'next/dynamic'
const MapWidget = dynamic(() => import('./MapWidget'), {
ssr: false,
loading: () => <div style={{ height: 320 }} aria-hidden />,
})
export default function LocationPanel() {
return (
<section>
<h2>Where your order is</h2>
<MapWidget />
</section>
)
}go deeper
Recall that ssr: false means the component is skipped during server rendering and appears only after the page loads in the browser, and that it belongs in a file marked 'use client'.
Explain what the server actually emits for that slot, why libraries touching window at import time need the option, and why a Server Component has no render pass for the option to defer to.
Demonstrate judgment about the cost: empty initial HTML, layout shift, nothing for crawlers. Show the narrower alternatives — doing DOM work in an effect, or splitting only the browser-dependent part into its own component.
Own where this belongs in the architecture: which surfaces may legitimately be client-only, how the team keeps browser-only dependencies from creeping into shared components, and how that constraint feeds vendor and library choices.
## What the option actually switches off `next/dynamic` normally does two things: it splits the component into its own chunk, and it still lets the component participate in server rendering, so its markup appears in the initial HTML and hydrates on the client. `ssr: false` removes the second half. ```tsx 'use client' import dynamic from 'next/dynamic' const MapWidget = dynamic(() => import('./MapWidget'), { ssr: false, loading: () => <div className="h-80" />, }) ``` During the server render, Next does not evaluate `MapWidget` at all. What lands in the HTML for that slot is the `loading` element, or an empty slot when you omit it. Once the page hydrates in the browser, the chunk is requested, the module is evaluated, and the component mounts and replaces the fallback. ## Why you would want that The standard reason is code that cannot survive a server environment. Server rendering runs in Node (or the Edge runtime), where there is no `window`, no `document`, no `localStorage`, and no DOM. A library that reads any of those at module scope throws the moment it is imported on the server, and one that reads them during render throws on the first render pass. Mapping libraries, charting libraries built on canvas, editors that measure DOM nodes, and anything wrapping a browser-only SDK are the usual suspects. A second reason is components whose server-rendered output would be wrong anyway — something that renders from a browser-only source of truth, so the server's version would only differ from the client's. Note that `ssr: false` is a blunt tool for the first problem. If only *part* of the component needs the DOM, the narrower fix is to keep the component server-renderable and do the browser work in an effect, which runs only in the browser by definition. ## Why the App Router rejects it in a Server Component In the App Router as of Next 15 and 16, `dynamic(..., { ssr: false })` in a file that is not marked `'use client'` fails the build, with an error directing you to move it into a Client Component. The reason is structural. A Server Component renders once, on the server, and its output is serialized into the payload the browser receives; it has no counterpart that re-renders in the browser. "Do not render this on the server, render it on the client instead" therefore has no client-side render pass to fall back to. The option only means something inside the client tree, where there genuinely are two render passes and you are choosing to skip one. The fix is mechanical: put the `dynamic()` call in a small `'use client'` module and render that module from the Server Component. ```tsx // map-widget.client.tsx 'use client' import dynamic from 'next/dynamic' export const MapWidget = dynamic(() => import('./MapWidget'), { ssr: false }) ``` ```tsx // page.tsx — a Server Component import { MapWidget } from './map-widget.client' export default function Page() { return <MapWidget /> } ``` ## What it costs Three costs are worth naming, because interviewers probe for whether you treat `ssr: false` as free: **Nothing in the initial HTML.** That region is blank until hydration finishes *and* the extra chunk arrives. If the component is above the fold, you have made the page look slower even though total bytes did not change. **Layout shift.** An empty slot that later fills with a 400 px widget pushes content down. This is why the `loading` fallback should reserve the real dimensions rather than being a bare spinner. **No content for crawlers or no-JS clients.** The markup simply is not there. Never hide primary content behind `ssr: false`. ## The common mix-up `ssr: false` and plain `dynamic()` solve different problems, and candidates routinely conflate them. Plain `dynamic()` is about *bundle timing* — the component still server-renders, you just fetch its JavaScript later. `ssr: false` is about *execution environment* — the component never touches the server. You can want the first without the second, and if your only goal is a smaller first-load bundle, adding `ssr: false` buys you nothing and costs you the server-rendered HTML.
- You need ssr: false but the component is used from a Server Component page. What is the smallest change that makes it legal?Move the `dynamic()` call itself into a tiny module that starts with `'use client'`, export the wrapped component from there, and import that wrapper in the Server Component. The Server Component now renders an ordinary Client Component, and the opt-out lives where a client render pass actually exists.
- A library only touches window inside a click handler, not at import time. Does it still need ssr: false?No. Handlers only run in the browser, so server rendering never executes that code. `ssr: false` is for modules that read browser globals at import time or during render. Reaching for it when a plain dynamic import would do costs you the server-rendered markup for no benefit.
- What should the loading fallback look like for a component loaded with ssr: false?Something that occupies the real component's dimensions — a sized placeholder or skeleton, not a small centred spinner. Because the slot is empty in the initial HTML, whatever arrives will push surrounding content unless the space was reserved, and that shift is visible to users and measurable in layout-stability metrics.
saying these in an interview costs you the question
- Thinks ssr: false makes the bundle smaller than a plain dynamic import
- Says the component renders on the server and is then discarded
- Uses ssr: false for above-the-fold or SEO-critical content
- Believes ssr: false works anywhere, including Server Components
- Ships it with no fallback and calls the resulting layout shift unavoidable