Next.js App Router でサーバーコンポーネントから dynamic ssr:false が使えない
App Router のサーバーコンポーネント内で next/dynamic に ssr: false を渡すとビルドエラーになる。
ssr: false はクライアントコンポーネントの中でのみ指定でき、用途が SSR off ならクライアントラッパ、遅延ロードだけなら Suspense で分離する。
公開: 更新:
要約
App Router で、サーバーコンポーネントから dynamic(..., { ssr: false }) を呼ぶとビルド時に「ssr: false is not allowed with next/dynamic in Server Components」エラーが出る。'use client' を付けた薄いラッパを挟むのが定石。
Pages Router の感覚で page.tsx に直接書いていた場合に頻発する。
実行例
node:20 のコンテナで Next.js 15.5.26 を使うと、サーバーコンポーネントの page.js から ssr: false 付きの dynamic を直接呼んだ段階で next build が「ssr: false is not allowed with next/dynamic in Server Components」で失敗し、終了コード 1 になった。"use client" を付けた ChartClient.js の中で呼ぶ形でも、ssr: false を外して React.lazy と Suspense で読み込む形でも、ビルドは終了コード 0 で通る。
$ cat app/page.js
import dynamic from "next/dynamic";
const Chart = dynamic(() => import("./_components/Chart"), { ssr: false });
export default function Page() {
return <Chart />;
}$ npx next build
▲ Next.js 15.5.26
Creating an optimized production build ...
Failed to compile.
./app/page.js
Error: x `ssr: false` is not allowed with `next/dynamic` in Server Components. Please move it into a Client Component.
,-[/tmp/tmp.WpiXADqbG1/app/page.js:3:1]
1 | import dynamic from "next/dynamic";
2 |
3 | const Chart = dynamic(() => import("./_components/Chart"), { ssr: false });
: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4 |
5 | export default function Page() {
6 | return <Chart />;
`----
Import trace for requested module:
./app/page.js
> Build failed because of webpack errors
$ echo $?
1$ cat app/page.js
import ChartClient from "./_components/ChartClient";
export default function Page() {
return <ChartClient />;
}$ npx next build
▲ Next.js 15.5.26
Creating an optimized production build ...
✓ Compiled successfully in 2.2s
Linting and checking validity of types ...
Collecting page data ...
Generating static pages (0/4) ...
Generating static pages (1/4)
Generating static pages (2/4)
Generating static pages (3/4)
✓ Generating static pages (4/4)
Finalizing page optimization ...
Collecting build traces ...
Route (app) Size First Load JS
┌ ○ / 1.25 kB 104 kB
└ ○ /_not-found 992 B 104 kB
+ First Load JS shared by all 103 kB
├ chunks/255-2dbbf79f36f0dfa2.js 46.4 kB
├ chunks/4bd1b696-c023c6e3521b1417.js 54.2 kB
└ other shared chunks (total) 1.94 kB
○ (Static) prerendered as static content
$ echo $?
0$ cat app/page.js
import { Suspense, lazy } from "react";
const Chart = lazy(() => import("./_components/Chart"));
export default function Page() {
return (
<Suspense fallback={<p>loading...</p>}>
<Chart />
</Suspense>
);
}$ npx next build
▲ Next.js 15.5.26
Creating an optimized production build ...
✓ Compiled successfully in 2.1s
Linting and checking validity of types ...
Collecting page data ...
Generating static pages (0/4) ...
Generating static pages (1/4)
Generating static pages (2/4)
Generating static pages (3/4)
✓ Generating static pages (4/4)
Finalizing page optimization ...
Collecting build traces ...
Route (app) Size First Load JS
┌ ○ / 255 B 103 kB
└ ○ /_not-found 992 B 103 kB
+ First Load JS shared by all 102 kB
├ chunks/255-2dbbf79f36f0dfa2.js 46.4 kB
├ chunks/4bd1b696-c023c6e3521b1417.js 54.2 kB
└ other shared chunks (total) 1.88 kB
○ (Static) prerendered as static content
$ echo $?
0— 2026-09-24 時点の出力
検証環境
- 検証日
- 実行環境
node:20Debian GNU/Linux 12 (bookworm)- バージョン
- Node.js 20.20.2
- npm 10.8.2
- Python 3.11.2
- Git 2.39.5
この記事の「実行例」は、上記の環境で実際にコマンドを実行して得られた出力をそのまま掲載しています。 再現手順はリポジトリの検証スクリプトとして管理し、定期的に再実行して出力を更新しています。
よくある原因
- Pages Router の感覚で
page.tsx(既定でサーバーコンポーネント)に直接dynamicを書いた - 取り込みたいコンポーネントが
window依存で SSR できない - クライアントツリーとサーバーツリーの境界が曖昧で、
use clientの置き場所が定まらない - Tree shaking でクライアント側に紛れ込ませようと
ssr: falseを多用している
解決策
1. クライアントラッパを作る
// app/_components/ChartClient.tsx
"use client";
import dynamic from "next/dynamic";
const Chart = dynamic(() => import("./Chart"), { ssr: false });
export default function ChartClient(props: ChartProps) {
return <Chart {...props} />;
}// app/dashboard/page.tsx (Server Component)
import ChartClient from "../_components/ChartClient";
export default function Page() {
return <ChartClient data={...} />;
}dynamic 呼び出しが "use client" 配下になることで、ssr: false が有効になる。
詳細は Lazy Loading 公式ドキュメントの next/dynamic(新しいタブで開く) を参照。
2. <Suspense> で代替する
SSR を完全に切る必要がなく「読み込みを分けて遅延させたい」だけなら、ssr: false を外して <Suspense> を使う:
import { Suspense, lazy } from "react";
const Heavy = lazy(() => import("./Heavy"));
export default function Page() {
return (
<Suspense fallback={<Spinner />}>
<Heavy />
</Suspense>
);
}React.lazy で読み込んだクライアントコンポーネントも既定でサーバー側で描画(SSR)されるため、初期 HTML には fallback ではなく実体が入り、クライアントはそれをハイドレートする。
初期 HTML に出さずブラウザでだけ描画したいなら、解決策 1 の ssr: false を使う。
3. クライアント側に依存ロジックを集約
window.localStorage などブラウザ API が必要な処理は、コンポーネントの内部でも useEffect の中で呼ぶ。
サーバーで実行されない経路にロジックを置けば、そもそも ssr: false を指定する必要がないことが多い。
設計指針は Lazy Loading 公式ドキュメント(新しいタブで開く) を参照。