next/dynamicで重いコンポーネントを遅延読み込みする
next/dynamicで重いコンポーネントを遅延読み込みする
ページの初期表示がなんとなく重い、と思って調べたら、開いてすぐには見えないモーダルやグラフ描画のライブラリが、最初のJavaScriptバンドルに丸ごと入っていました。そのときのメモです。
結論として、初期表示に不要なコンポーネントは next/dynamic で 必要になったときに読み込む ようにすると、最初に読み込むJavaScriptを小さくできます。体感の初期表示がだいぶ軽くなりました。
何が起きているのか
通常の import で読み込んだコンポーネントは、それを使うページのバンドルに含まれます。つまり、画面にすぐ出ないコンポーネントでも、ページを開いた時点でコードがダウンロード・パースされます。
import HeavyChart from './HeavyChart' // 重いグラフライブラリに依存
export default function Page() {
const [open, setOpen] = useState(false)
return (
<div>
<button onClick={() => setOpen(true)}>グラフを見る</button>
{open && <HeavyChart />}
</div>
)
}
HeavyChart はボタンを押すまで表示されないのに、ページを開いた瞬間にそのコードは読み込まれています。これがバンドルを膨らませる原因でした。初期表示の指標にも効いてくるので、Core Web Vitalsの記事で触れた数値が悪いときは、ここも疑ってみるとよさそうです。
next/dynamic で遅延読み込みにする
next/dynamic を使うと、コンポーネントを 別のチャンク に切り出し、実際に描画されるタイミングで読み込めます。書き方は以下のようになります。
import dynamic from 'next/dynamic'
import { useState } from 'react'
const HeavyChart = dynamic(() => import('./HeavyChart'))
export default function Page() {
const [open, setOpen] = useState(false)
return (
<div>
<button onClick={() => setOpen(true)}>グラフを見る</button>
{open && <HeavyChart />}
</div>
)
}
import('./HeavyChart') という 動的インポート を dynamic() に渡すのがポイントです。これで HeavyChart のコードはページ初期表示のバンドルから外れ、open が true になって実際に描画されるときに読み込まれます。最初に読むJavaScriptが減るので、初期表示が軽くなるわけです。
読み込み中の表示を出す
遅延読み込みは、コードを取りに行くあいだ少し待ちが発生します。その間に何も出ないと不自然なので、loading でプレースホルダを指定できます。
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <p>グラフを読み込み中…</p>,
})
ここで指定したコンポーネントが、本体の読み込みが終わるまで表示されます。一瞬の空白を埋められるので、入れておくと体感がよくなります。
ブラウザ専用のコンポーネントは ssr: false
window や document を前提にしたライブラリ(地図やエディタ系に多い印象です)は、サーバー側でレンダリングしようとすると落ちることがあります。その場合は ssr: false でサーバーレンダリングを切り、ブラウザ側だけで描画します。
const MapView = dynamic(() => import('./MapView'), {
ssr: false,
loading: () => <p>地図を準備中…</p>,
})
これでサーバーでは描画されず、ブラウザに届いてから読み込まれます。window is not defined のようなエラーで困ったときは、まずこれを疑うとよさそうです。
ただし注意点として、ssr: false にしたコンポーネントはサーバー側のHTMLに含まれません。検索エンジンに拾ってほしい本文や、初期表示で必ず見せたい要素には使わないほうがよいです。「最初は見えなくてよくて、操作で初めて出てくるもの」に限定するのが安全だと思います。
なお、App Router の Server Components では ssr: false は指定できません(クライアントコンポーネントの中で使う必要があります)。このあたりはServerとClient Componentsの記事の使い分けと合わせて考えると整理しやすいです。
何でも遅延すればいいわけではない
便利なので全部 dynamic にしたくなりますが、やりすぎると逆効果なこともあります。
- 初期表示で必ず見えるもの を遅延すると、表示が遅れてかえって体感が悪くなります。ファーストビューの要素は通常の
importのままがよいです。 - チャンクが細かく分かれすぎると、リクエスト数が増えてオーバーヘッドになることがあります。小さなコンポーネントを無理に切り出す必要はありません。
効果が大きいのは「重いライブラリに依存していて、かつ初期表示には出ない」コンポーネントです。next build の出力でチャンクサイズを見て、大きいものから手を付けるのが現実的だと思います。
まとめ
- 通常の
importは、すぐ表示されないコンポーネントも初期バンドルに含めてしまう next/dynamic+ 動的インポートで、必要になったときに読み込めて初期バンドルが軽くなるloadingで読み込み中の表示を、ssr: falseでブラウザ専用ライブラリに対応できる- 初期表示に必要なものまで遅延しない。効くのは「重い・かつ最初は見えない」もの
まずは一番重そうなコンポーネント一つを dynamic に置き換えて、next build のチャンクサイズがどう変わるか見てみるのがおすすめです。詳しい指定方法はNext.js公式ドキュメント(Lazy Loading)も参考になります。以上です。