With next/dynamic, ssr: false skips prerendering one component: the server sends its loading fallback, and the browser loads the component's code and renders it after hydration. Both renders React compares show the fallback, so the component cannot cause a mismatch. In the App Router, the call must be inside a Client Component.
How next/dynamic ssr: false works
next/dynamic is a composite of React.lazy() and Suspense, and it "behaves the same way in the app and pages directories" (Next.js lazy loading docs). By default, a dynamically imported Client Component is still prerendered. With ssr: false, it is not:
"use client";
import dynamic from "next/dynamic";
// The map library reads window when it is imported.
const StoreMap = dynamic(() => import("./store-map"), {
ssr: false,
loading: () => <div className="h-96 rounded bg-muted" />,
});
export function StoreFinder() {
return <StoreMap />;
}What happens on a page load:
- The server renders
StoreFinderand puts theloadingfallback where the map goes. The map's module does not run on the server. - Hydration renders the fallback in the same place, so it matches.
- The browser loads the map's chunk and replaces the fallback with the map.
Because the module does not run on the server, this is the fix for libraries that touch window or document at import time, which a plain client-only component cannot help with.
Next.js dynamic import (ssr: false) in the App Router
Since Next.js 15, ssr: false is not allowed in Server Components. Calling it in app/page.tsx without "use client" fails with:
`ssr: false` is not allowed with `next/dynamic` in Server Components. Please move it into a Client Component.Move the call into a Client Component and render that from the page:
// A Server Component: no "use client", no ssr: false here.
import { StoreFinder } from "./store-finder";
export default function Page() {
return (
<main>
<h1>Find a store</h1>
<StoreFinder />
</main>
);
}store-finder.tsx above is the Client Component that holds the dynamic() call. The Next.js docs add that ssr: false only works for Client Components and that the client code-splitting works properly only there.
In the Pages Router there are no Server Components, so dynamic(() => import("…"), { ssr: false }) works in any page or component.
When to use ssr: false
Use it when a component cannot render on the server at all:
- a library that reads
window,documentornavigatorwhen it is imported (maps, some chart and editor libraries); - a widget whose output depends entirely on the browser, where a server version would be wrong anyway;
- a large component you want out of the first bundle, which also should not block the first paint.
Do not use it to silence a mismatch in content that could render on the server. A date, a price or a user's name can almost always be rendered the same way on both sides, and search engines and link previews then get the content. The causes guides show how.
ssr: false vs a ClientOnly component
next/dynamic with ssr: false | ClientOnly wrapper | |
|---|---|---|
| Server HTML | The loading fallback | The fallback prop |
| Module on the server | Not run | Imported and run, not rendered |
| Code splitting | A separate chunk | Bundled with the page |
| Where it can be used | Client Components (App Router), any file (Pages Router) | Anywhere, including Server Components as a parent |
Check the result
A correct ssr: false component produces no hydration finding. What goes wrong is usually around it: a fallback of a different size shifts the layout, or a sibling still reads the browser during render. hydration-proof tests every route, so a component that moved from the server render to the client render is checked on every page that uses it:
npx hydration-proof test