The next/dynamic ssr: false option is for a component that cannot run on the server. The server shows a fallback. The browser loads and displays the component after hydration. In the App Router, put the dynamic() call in a Client Component.
How next/dynamic ssr: false works
Dynamic imports do not automatically skip server rendering. By default, Next.js still prerenders a dynamically imported Client Component. Add ssr: false to skip that render (Next.js lazy loading docs):
"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 />;
}On page load:
- The server renders the loading fallback. It does not import the map module.
- The browser renders the same fallback during hydration.
- Once the map loads, the browser replaces the fallback with the map.
This also works for libraries that read window or document as soon as they are imported. A ClientOnly wrapper hides a component's output on the server, but it does not stop its module from being imported there.
Next.js dynamic import (ssr: false) in the App Router
In the App Router, you cannot call dynamic() with ssr: false from a Server Component. Next.js reports:
`ssr: false` is not allowed with `next/dynamic` in Server Components. Please move it into a Client Component.Keep the page as a Server Component. Move the dynamic import into a child Client Component, then render that child 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>
);
}Here, store-finder.tsx is the Client Component that holds the dynamic() call.
In the Pages Router, you can use dynamic(() => import("…"), { ssr: false }) in a page or component.
When to use ssr: false
Use ssr: false for components that need the browser before they can render:
- A map, chart, or editor library that reads
window,document, ornavigatoron import. - A widget that has no useful server-rendered version.
- A large browser-only component that can load separately from the initial page.
Do not use it just to hide a hydration error. If a date, price, or name can be rendered consistently, keep it in the server HTML. That way, people and crawlers can see the content before JavaScript loads. See the causes guides for ways to fix mismatches at their source.
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
Run the check after adding the dynamic import:
npx hydration-proof testCheck the page visually too. If the fallback and loaded component have different sizes, the page may shift even though hydration succeeds. If you still get a finding, check nearby components for browser-only values used during render.