ページを表示した瞬間には使わない重いコンポーネント、たとえばグラフやリッチエディタ、クリックして初めて開くモーダルなどを、最初からまとめて読み込んでしまうと初期表示が遅くなります。Next.js には、こうしたコンポーネントを必要になったときだけ後から読み込むための next/dynamic という仕組みが用意されています。この記事では、App Router を前提に、dynamic() の基本的な書き方、読み込み中のUIを出す loading オプション、サーバーサイドレンダリングを無効化する ssr: false、名前付きエクスポートの読み込み方、そしてクリック時にモーダルを読み込む実用例までを解説します。
目次
next/dynamic とは何か
next/dynamic は、コンポーネントを動的インポート(遅延読み込み)するための Next.js の機能です。dynamic() という関数にインポート処理を渡すと、そのコンポーネントは最初のバンドルには含まれず、実際に画面に表示されるタイミングで別ファイルとして読み込まれます。中身としては React の React.lazy と Suspense を包んだラッパーで、そこに loading や ssr といった Next.js 独自のオプションを足したものだと考えると分かりやすいでしょう。
通常の import でコンポーネントを読み込むと、そのコードは最初に配信される JavaScript バンドルへ一緒に含まれます。ページを開くうえで必ず必要なコンポーネントであれば問題ありませんが、最初は見えていない・すぐには使わないコンポーネントまで含まれていると、その分だけ初期バンドルが大きくなり、ページが操作可能になるまでの時間が延びてしまいます。next/dynamic はこの一部を「後回し」にする手段です。
なぜ動的インポートを使うのか
動的インポートの一番の目的は、初期バンドルを小さくして初期表示を速くすることです。ページを表示した直後に必要なコードだけを先に読み込み、それ以外は後から取りに行くようにすれば、最初にダウンロード・解析する JavaScript の量が減り、表示や操作が速くなります。
効果が出やすいのは、重くて、かつ最初は使わないコンポーネントです。具体的にはチャート(グラフ描画)ライブラリを使う画面、コードエディタやリッチテキストエディタ、地図、そしてボタンを押して初めて開くモーダルやダイアログなどが典型例です。これらを next/dynamic で必要になったときだけ読み込むようにすると、初期表示のコストを大きく下げられます。逆に、ページの中心となる常に見えているコンポーネントを動的インポートしても効果は薄く、かえって読み込みのタイミングが遅れて表示のちらつきを招くこともあるため、対象は見極めて選ぶのがよいでしょう。
基本の書き方
基本形はとてもシンプルです。next/dynamic から dynamic をインポートし、import() を返す関数を渡すだけです。返ってきた値を、あとは普通のコンポーネントと同じように JSX の中で使えます。
import dynamic from 'next/dynamic';
// HeavyComponent を動的インポートする
// この時点ではまだ読み込まれず、描画されるときに読み込まれる
const HeavyComponent = dynamic(() => import('./HeavyComponent'));
export default function Page() {
return (
<main>
<h1>トップページ</h1>
{/* 普通のコンポーネントと同じように使える */}
<HeavyComponent />
</main>
);
}
dynamic(() => import('./HeavyComponent')) の戻り値を HeavyComponent という定数で受け取り、あとは <HeavyComponent /> のように書くだけです。読み込みの仕組みは next/dynamic が裏側で面倒を見てくれるので、使う側のコードはほとんど変わりません。読み込む対象のファイルは、export default でコンポーネントを1つ書いた普通のファイルで構いません。
loading オプションで読み込み中のUIを表示する
動的インポートされたコンポーネントは、実際に読み込みが終わるまでの間わずかな空白の時間が生じます。この間に表示するプレースホルダーを、第2引数の loading オプションで指定できます。loading には、読み込み中に表示したい内容を返すコンポーネントを渡します。
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('./Chart'), {
// 読み込みが終わるまで表示される
loading: () => <p>グラフを読み込んでいます…</p>,
});
export default function Page() {
return (
<main>
<h1>ダッシュボード</h1>
<Chart />
</main>
);
}
これで、Chart の読み込みが完了するまでは「グラフを読み込んでいます…」というメッセージが表示され、読み込みが終わると本来のコンポーネントに切り替わります。スピナーやスケルトン(枠だけの仮表示)をここに置いておくと、読み込み中でもレイアウトが崩れず、体感の待ち時間を和らげられます。
ssr: false でクライアントだけで描画する
next/dynamic には、サーバーサイドレンダリングを無効化する ssr オプションがあります。ssr: false を指定すると、そのコンポーネントはサーバー側では描画されず、ブラウザ(クライアント)でのみ描画されるようになります。
これが必要になるのは、window や document といったブラウザ固有のオブジェクトに依存するライブラリを使うときです。サーバー側にはこれらが存在しないため、そのまま SSR すると実行時にエラーになります。ssr: false にしておけば、サーバーでは描画をスキップし、ブラウザ側でだけ読み込んで動かせるので、こうしたエラーを避けられます。
'use client';
import dynamic from 'next/dynamic';
// window に依存するマップライブラリを想定
// サーバーでは描画せず、ブラウザだけで読み込む
const Map = dynamic(() => import('./Map'), {
ssr: false,
loading: () => <p>地図を準備しています…</p>,
});
export default function MapView() {
return <Map />;
}
ここで押さえておきたいオプションを表にまとめます。dynamic() の第2引数に、オブジェクトの形で渡します。
| オプション | 説明 |
|---|---|
loading | 読み込みが完了するまで表示するコンポーネントを指定する |
ssr | false にするとサーバーサイドレンダリングを無効化し、クライアントだけで描画する(既定は true) |
Server Component で ssr:false がエラーになるとき
ssr: false を使うときに、Next.js 15 で特に注意したい点があります。ssr: false は Server Component の中では使えず、Client Component の中でのみ指定できるという仕様です。App Router のコンポーネントは既定で Server Component になるため、ファイルの先頭に 'use client' を書かずに ssr: false を使うと、ビルド時にエラーになります。
解決策はシンプルで、ssr: false を使う dynamic() の呼び出しを、'use client' を付けた Client Component の中に置くことです。先ほどの MapView.tsx の例でファイルの先頭に 'use client' を書いていたのは、このためです。
'use client'; // これがないと ssr: false でエラーになる
import dynamic from 'next/dynamic';
const Map = dynamic(() => import('./Map'), { ssr: false });
export default function ClientMap() {
return <Map />;
}
このように ssr: false を含む部分を Client Component として切り出し、それを Server Component 側から <ClientMap /> のように呼び出せば、Server Component のツリーを保ったまま、ブラウザ限定のコンポーネントだけを安全に描画できます。ssr: false を付けていない dynamic() はこの制約を受けないため、単に初期バンドルを分割したいだけなら Server Component でもそのまま使えます。
名前付きエクスポートを動的インポートする
dynamic() に渡す import() は、既定では default エクスポートを読み込みます。読み込みたいコンポーネントが export default ではなく名前付きエクスポートの場合は、import() の後ろに .then() をつないで、欲しいコンポーネントを取り出して返します。
import dynamic from 'next/dynamic';
// Editor.tsx が「export function Editor() { ... }」のように
// 名前付きでエクスポートしている場合
const Editor = dynamic(() =>
import('./Editor').then((mod) => mod.Editor)
);
export default function Page() {
return <Editor />;
}
import('./Editor') はモジュール全体を表すオブジェクトを返すので、.then((mod) => mod.Editor) でその中の Editor を選んで返しています。default エクスポートなら .then() は不要ですが、名前付きの場合はこの一手間で対象を指定する、と覚えておくとよいでしょう。
クリックで初めてモーダルを読み込む実用例
動的インポートが効果を発揮しやすい典型例が、ボタンを押したときに初めて開くモーダルです。モーダルの中身は最初は画面に見えていないので、ページの初期表示のために先読みする必要はありません。ボタンが押されたタイミングで初めて読み込むようにすれば、そのコード分だけ初期バンドルを軽くできます。
'use client';
import { useState } from 'react';
import dynamic from 'next/dynamic';
// Modal は開くまで読み込まれない
const Modal = dynamic(() => import('./Modal'), {
loading: () => <p>読み込み中…</p>,
});
export default function OpenModalButton() {
const [open, setOpen] = useState(false);
return (
<>
<button onClick={() => setOpen(true)}>詳細を開く</button>
{/* open が true になった瞬間に Modal が読み込まれる */}
{open && <Modal onClose={() => setOpen(false)} />}
</>
);
}
ポイントは、{open && <Modal ... />} のように、状態が真になったときだけ Modal を JSX に登場させている点です。dynamic() による読み込みは、そのコンポーネントが実際に描画されようとしたときに始まります。ボタンを押して open が true になるまで Modal は描画されないため、モーダルのコードもそのタイミングまで読み込まれません。こうしたオンデマンドの読み込みによって、最初にユーザーが目にしないコードを後回しにできます。
まとめ
next/dynamic の dynamic() は、コンポーネントを動的インポートして遅延読み込みするための仕組みで、React の lazy と Suspense に loading や ssr のオプションを足したものです。const HeavyComponent = dynamic(() => import('./HeavyComponent')) と書けば、あとは普通のコンポーネントと同じように使えます。loading オプションで読み込み中のUIを差し込め、ssr: false でサーバーサイドレンダリングを無効化してブラウザ限定で描画できます。ただし Next.js 15 では ssr: false は Client Component('use client')の中でしか使えない点に注意してください。名前付きエクスポートは .then((mod) => mod.Named) で取り出し、クリックで開くモーダルのように「使うときだけ読み込む」構成にすれば、初期バンドルを小さく保てます。重くて最初は使わないコンポーネントを見つけたら、next/dynamic の適用を検討してみましょう。