本記事に関して
Next.jsは レンダリング戦略 を細かく制御できることが強みとされています。ただそれが実際にパフォーマンスやUXにどれほどの影響があるのかがわからなかったため、各レンダリング戦略が体感でき、且つパフォーマンスが計測できるサイトを作ってみました。
サーバーサイドレンダリング (SSR)、静的サイト生成 (SSG)、クライアントサイドレンダリング (CSR)、パーシャル・プリレンダリング (PPR)の4つが比較できるようになっています。
https://next-performance-test-ten.vercel.app/
技術選定と実装内容
技術選定
使用言語・フレームワーク
- 言語: TypeScript 5
- フレームワーク: Next.js 15.3.6
- UIライブラリ: React 19.0.0
- スタイリング: Tailwind CSS 4
- 開発環境: Turbopack
- ホスティング: Vercel
計測ツール
-
Web Vitals: web-vitals 5.0.3
- 標準のCore Web Vitals(FCP, LCP, TTFB, INP)を計測
-
カスタムメトリクス:
-
TTCV(Time To Content Visible): 最初のカードが表示されるまでの時間 -
ListRendered: リストのDOM描画完了時間 -
FirstImageLoaded: 最初の画像の読み込み完了時間
-
-
Server-Timing API: サーバーサイドの処理時間を詳細に計測
- アプリケーション層の処理時間
- 外部API取得時間
実装内容
リポジトリは以下に公開しています
データフェッチの仕組み
各レンダリング戦略で異なるデータフェッチ方法を採用しています。
1. CSR (Client-Side Rendering)
- 内部API (
/api/posts) 経由でJSONPlaceholder APIからデータを取得 - クライアントサイドで400msの人工遅延を追加
2. SSR (Server-Side Rendering)
- 内部API経由でpostsとusersを並列取得
- サーバーサイドでデータを統合・加工
- 700msの人工遅延を追加(DB結合や集計処理を模倣)
3. SSG (Static Site Generation)
- ビルド時にJSONPlaceholder APIから直接データ取得
-
force-staticで静的生成を強制
4. PPR風 (Streaming + Suspense)
- 静的部分は即座に表示、動的部分はSuspenseでストリーミング
- 動的コンテンツは600msの遅延後にストリーミング配信
人工遅延の設定
リアルな環境を模倣するため、各箇所に人工的な遅延を追加しています。
| エンドポイント/処理 | 遅延時間 | 目的 |
|---|---|---|
/api/posts |
300ms | サーバー処理・外部API取得の遅延を模倣 |
/api/users |
150ms | 軽量なAPI取得を想定 |
| SSR追加処理 | 700ms | DB結合や複雑な集計処理を模倣 |
| CSRクライアント処理 | 400ms | JS実行・API待ちの体感差を強調 |
| PPR動的部分 | 600ms | 動的コンテンツ取得の遅延を模倣 |
これらの遅延により、各レンダリング戦略のパフォーマンス特性を明確に比較できるようになっています。
今回はAPIやページの表示内容が軽量のため、人口遅延を入れないと計測結果にほとんど差がでませんでした。つまりは軽量なページであればどの手法でもさほどUXに影響はないです。ただしSEOには影響があるのでご注意ください。
測定結果
サーバーサイドレンダリング (SSR)
| メトリクス | 1回目 (ms) | 2回目 (ms) | 3回目 (ms) | 4回目 (ms) | 5回目 (ms) | 平均 (ms) |
|---|---|---|---|---|---|---|
| TTFB | 44 | 30 | 29 | 40 | 33 | 35 |
| FCP | 112 | 192 | 72 | 104 | 112 | 118 |
| LCP | 112 | 192 | 72 | 104 | 112 | 118 |
| TTCV | 2220 | 1559 | 1582 | 1418 | 1356 | 1627 |
| ListRendered | 2214 | 1554 | 1578 | 1414 | 1350 | 1622 |
| FirstImageLoaded | 2225 | 1554 | 1578 | 1408 | 1345 | 1622 |
静的サイト生成 (SSG)
| メトリクス | 1回目 (ms) | 2回目 (ms) | 3回目 (ms) | 4回目 (ms) | 5回目 (ms) | 平均 (ms) |
|---|---|---|---|---|---|---|
| TTFB | 41 | 42 | 36 | 35 | 43 | 39 |
| FCP | 124 | 112 | 112 | 148 | 124 | 124 |
| LCP | 124 | 112 | 112 | 148 | 124 | 124 |
| TTCV | 45 | 38 | 41 | 40 | 51 | 43 |
| ListRendered | 40 | 33 | 37 | 36 | 48 | 39 |
| FirstImageLoaded | 40 | 33 | 37 | 31 | 42 | 37 |
クライアントサイドレンダリング (CSR)
| メトリクス | 1回目 (ms) | 2回目 (ms) | 3回目 (ms) | 4回目 (ms) | 5回目 (ms) | 平均 (ms) |
|---|---|---|---|---|---|---|
| TTFB | 36 | 35 | 82 | 39 | 34 | 45 |
| FCP | 120 | 120 | 164 | 104 | 104 | 122 |
| LCP | 120 | 120 | 164 | 104 | 104 | 122 |
| TTCV | 1304 | 1275 | 1340 | 1383 | 1346 | 1330 |
| ListRendered | 1300 | 1270 | 1337 | 1378 | 1343 | 1326 |
| FirstImageLoaded | 1300 | 1270 | 1332 | 1378 | 1337 | 1323 |
パーシャル・プリレンダリング (PPR)
| メトリクス | 1回目 (ms) | 2回目 (ms) | 3回目 (ms) | 4回目 (ms) | 5回目 (ms) | 平均 (ms) |
|---|---|---|---|---|---|---|
| TTFB | 41 | 43 | 35 | 41 | 47 | 41 |
| FCP | 116 | 108 | 104 | 120 | 128 | 115 |
| LCP | 116 | 108 | 104 | 120 | 128 | 115 |
| TTCV | 1350 | 1216 | 1276 | 1193 | 1258 | 1259 |
| ListRendered | 1346 | 1213 | 1273 | 1189 | 1255 | 1255 |
| FirstImageLoaded | 1345 | 1205 | 1267 | 1184 | 1250 | 1250 |
期待される傾向、結果の分析
TTFB (Time To First Byte)
サーバーがリクエストを受け取ってから、レスポンスの最初の1バイトをブラウザに送信するまでの時間です。
期待される傾向
- SSG (静的生成) と PPR (部分的な事前レンダリング) は、事前にHTMLが生成・キャッシュされるため、非常に短くなる傾向があります。SSR (サーバーサイドレンダリング) と CSR (クライアントサイドレンダリング) は、リクエストごとに処理が走るため、SSG/PPRより長くなります。特にSSRは毎回サーバーでHTMLを生成するため長くなりがちです。
FCP (First Contentful Paint) & LCP (Largest Contentful Paint)
FCP: 画面に最初のコンテンツ(テキスト、画像など)が描画されるまでの時間です。
LCP: 画面内の最大コンテンツ要素(メイン画像や大きなテキストブロック)が描画されるまでの時間です。
期待される傾向:
-
SSG/SSR/PPRは、HTMLにコンテンツが含まれた状態で配信されるため、ブラウザは受け取り次第すぐに描画を開始でき、FCP/LCPが短くなる傾向があります。
-
CSRは、空のHTMLを受け取った後、JavaScriptをダウンロード・実行して初めてコンテンツを生成・描画するため、FCP/LCPが長くなる傾向があります。
TTV (Time To View/Total Time to View) & リスト描画時間
この計測では、「TTCV (Time To Contentful View)」や「ListRendered」「FirstImageLoaded」というカスタムメトリクスが使用されています。これらは、ページが完全に表示され、コンテンツが利用可能になるまでの時間を示す指標です。
期待される傾向:
- SSR/CSR/PPRは、データフェッチやJavaScriptの処理(ハイドレーションなど)が関与するため、SSGよりも時間がかかる傾向があります。
- SSGは静的なコンテンツなので、TTCVやListRenderedがFCP/LCPとほぼ同等で、非常に短くなることが期待されます。
- SSR/CSR/PPRは、FCP/LCPからTTCV/ListRenderedまでの間に、JavaScriptのハイドレーションや動的データのロード時間が発生し、時間の差が生まれる傾向があります。
結果の分析
✅️ 本来出るべき結果が出ている点
- SSG/PPRのTTFBが最も短い: 静的・キャッシュ可能なコンテンツの高速配信という利点が明確に出ています。
- SSR/CSR/PPRのTTCV/ListRenderedが長い: FCP/LCPとの間に大きな差があり、初期表示後のJavaScript処理(ハイドレーションなど)に時間がかかっているという動的なレンダリング戦略の一般的なトレードオフが明確に表れています。
- SSGが最終的なコンテンツ描画(TTCV/ListRendered)で最も速い: 静的なコンテンツは最速であるという基本原則通りです。
⚠️ 意外な点(良好な結果ではある)
- CSRのFCP/LCPがSSG/SSRとほぼ同等: 通常はCSRが最も遅くなるはずですが、この計測では非常に高速です。これは、計測対象のページが非常にシンプルで、Javascriptのバンドルサイズが小さいからであると考えられます。
感想
SSGが使える場合は積極的に使用すべきで、それが難しい場合はISRを使ってなるべく静的データを作っておくことで表示スピードが高速になりSEO対策にもなると思いました。また、SSRとCSRはレスポンススピードによってUXが左右される傾向があるため、個人的にはPPRが最もストレスフリーだと感じました。Next14以降はPPRはStableになったため積極的に使っていこうと思います。