Hydration Proof

Search documentation

Find a page or section

next/dynamic with ssr: false

Skip the server render for one component, and load its code in the browser.

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:

app/stores/store-finder.tsx
"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:

  1. The server renders StoreFinder and puts the loading fallback where the map goes. The map's module does not run on the server.
  2. Hydration renders the fallback in the same place, so it matches.
  3. 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:

app/stores/page.tsx
// 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, document or navigator when 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: falseClientOnly wrapper
Server HTMLThe loading fallbackThe fallback prop
Module on the serverNot runImported and run, not rendered
Code splittingA separate chunkBundled with the page
Where it can be usedClient 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