<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Web on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/tags/web/</link>
    <description>Recent content in Web on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Sun, 22 Feb 2026 18:00:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/tags/web/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Next.js 16の変更点まとめ：Partial Prerenderingが安定版に</title>
      <link>https://www.ai2core.com/posts/2026-02-22-nextjs-16/</link>
      <pubDate>Sun, 22 Feb 2026 18:00:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-02-22-nextjs-16/</guid>
      <description>動的コンテンツと静的コンテンツを混在させるPPRの正式リリースについて。</description>
      <content:encoded><![CDATA[<h1 id="nextjs-16の変更点まとめpartial-prerenderingが安定版に">Next.js 16の変更点まとめ：Partial Prerenderingが安定版に</h1>
<h2 id="はじめに">はじめに</h2>
<p>「Next.jsアプリのパフォーマンス、もっと向上させたい…」
「静的サイトのような爆速表示と、動的コンテンツのリアルタイム更新、この二つを両立できずに悩んでいませんか？」</p>
<p>Next.js開発者であれば、一度はこのような課題に直面したことがあるでしょう。私たちはこれまで、ページの特性に応じてServer-Side Rendering (SSR)、Static Site Generation (SSG)、Incremental Static Regeneration (ISR) といったレンダリング戦略を使い分ける必要がありました。</p>
<ul>
<li><strong>SSR</strong>は動的なコンテンツに強い反面、リクエストごとにサーバーでレンダリングするためTime to First Byte (TTFB) が遅くなりがちです。</li>
<li><strong>SSG</strong>はCDNから配信できるため非常に高速ですが、動的コンテンツの扱いやビルド時間の増大という課題を抱えています。</li>
<li><strong>ISR</strong>はSSGの弱点を補いますが、リアルタイムの更新には対応しきれない場面もあります。</li>
</ul>
<p>これらの戦略は強力ですが、ページ全体を「静的」か「動的」のどちらか一方の型にはめる「All or Nothing」のアプローチでした。例えば、大半が静的なEコマースの商品ページでも、在庫情報やレビューといった一部の動的コンテンツのために、ページ全体をSSRせざるを得ず、パフォーマンスを犠牲にすることが少なくありませんでした。</p>
<p>この長年のジレンマに終止符を打つべく登場し、Next.js 16でついに安定版となったのが <strong>Partial Prerendering (PPR)</strong> です。</p>
<p>PPRは、静的なパフォーマンスと動的な柔軟性を融合させる、まさに革命的なレンダリングモデルです。この記事では、PPRがなぜ重要なのか、その仕組み、具体的な実装方法から実践的なTipsまで、Next.js 16の目玉機能であるPPRを徹底的に解剖します。この記事を読み終える頃には、あなたはPPRを完全に理解し、自身のプロジェクトでその強力なパフォーマンスを最大限に引き出せるようになっているはずです。</p>
<h2 id="なぜpprは登場したのかwebレンダリングの進化と課題">なぜPPRは登場したのか？Webレンダリングの進化と課題</h2>
<p>PPRの革新性を理解するためには、まずこれまでのWebレンダリングがどのような変遷を辿ってきたのかを知る必要があります。</p>
<h3 id="webレンダリング戦略の歴史">Webレンダリング戦略の歴史</h3>
<ol>
<li>
<p><strong>サーバーサイドレンダリングの黎明期 (CGI, PHP, Ruby on Rails)</strong>
Webの初期、コンテンツはリクエストごとにサーバーでHTMLを生成して返すのが主流でした。これにより動的なコンテンツは実現できましたが、リクエストのたびに重い処理が走るため、パフォーマンスに課題がありました。</p>
</li>
<li>
<p><strong>SPA (Single Page Application) の台頭 (React, Vue, Angular)</strong>
次に、クライアントサイドでJavaScriptがUIを構築するSPAが登場しました。初回ロード後はページ遷移なしに高速な画面更新が可能になり、リッチなUIが実現できます。しかし、初回のバンドルサイズが大きく初期表示が遅い、そしてSEOに課題があるという弱点がありました。</p>
</li>
<li>
<p><strong>Next.jsがもたらしたハイブリッドアプローチ (SSR, SSG, ISR)</strong>
この両者の課題を解決するために登場したのがNext.jsです。Next.jsは、ページ単位で最適なレンダリング戦略を選択できるハイブリッドなフレームワークとして絶大な支持を得ました。</p>
<ul>
<li><strong>SSR (Server-Side Rendering):</strong> リクエストごとにサーバーでHTMLを生成。SEOに強く、初期表示に必要なデータが揃っています。しかし、サーバー負荷が高く、TTFBが遅くなりがちです。</li>
<li><strong>SSG (Static Site Generation):</strong> ビルド時にすべてのページをHTMLとして事前生成。CDNに配置することで、リクエスト時にはファイルを返すだけなので超高速です。しかし、ユーザー固有の情報のような動的コンテンツには向かず、サイト規模が大きくなるとビルド時間が長大になる問題がありました。</li>
<li><strong>ISR (Incremental Static Regeneration):</strong> SSGの弱点を補う仕組み。一定時間ごとにバックグラウンドでページを再生成し、静的コンテンツの鮮度を保ちます。しかし、リアルタイム性が求められるコンテンツには対応が難しいという側面がありました。</li>
</ul>
</li>
</ol>
<h3 id="従来のレンダリング戦略が抱えるall-or-nothing問題">従来のレンダリング戦略が抱える「All or Nothing」問題</h3>
<p>これらの戦略は非常に強力でしたが、根本的な課題を抱えていました。それは、<strong>1つのページは1つのレンダリング戦略しか選べない</strong>という「All or Nothing」の問題です。</p>
<p>この問題が顕著になるのが、静的コンテンツと動的コンテンツが混在するページです。先ほどのEコマースの商品ページを例に考えてみましょう。</p>
<ul>
<li><strong>静的な部分:</strong> 商品名、商品説明、スペック、メイン画像など、滅多に変わらない情報。</li>
<li><strong>動的な部分:</strong> 在庫数、ユーザーレビュー、あなたへのおすすめ商品、セール価格など、リアルタイムで変化する情報。</li>
</ul>
<p>従来のモデルでは、このページに動的な部分が一つでも含まれていると、ページ全体をSSRまたはISR（短い間隔での再生成）で構築する必要がありました。これにより、本来は高速に表示できるはずの静的な部分まで、リクエストのたびにサーバーでレンダリングされるのを待たなければならず、TTFBが悪化し、ユーザー体験を損なう原因となっていました。</p>
<p>この「All or Nothing」の制約を打ち破り、静的な部分のパフォーマンスと動的な部分の柔軟性を1つのページ内で共存させるために開発されたのが、Partial Prerendering (PPR)なのです。</p>
<h2 id="partial-prerendering-ppr-の核心に迫る仕組みと使い方">Partial Prerendering (PPR) の核心に迫る：仕組みと使い方</h2>
<p>PPRは、Next.js App RouterとReact Server Components (RSC) のアーキテクチャを基盤として実現されています。そのコンセプトは非常にシンプルです。「ページの静的な部分を事前にビルドしておき、動的な部分だけをリクエスト時にストリーミングで配信する」というものです。</p>
<h3 id="静的シェルと動的ホール">「静的シェル」と「動的ホール」</h3>
<p>PPRを理解する上で最も重要な概念が「静的シェル (Static Shell)」と「動的ホール (Dynamic Holes)」です。</p>
<ul>
<li><strong>静的シェル:</strong> ページのレイアウト、ナビゲーションバー、フッター、そして商品名や商品説明といった静的なコンテンツを含む、ページの骨格となる部分です。これはビルド時に事前生成（Prerender）され、CDNにキャッシュされます。</li>
<li><strong>動的ホール:</strong> 在庫情報やレビューなど、動的に生成されるコンテンツが入る「穴」の部分です。静的シェルの中では、この部分はプレースホルダーとして空けられています。</li>
</ul>
<p>この仕組みを図でイメージしてみましょう。</p>
<pre tabindex="0"><code>【ビルド時】
+-------------------------------------------------+
|               静的シェル (Static Shell)           |
| +-------------------------------------------+ |
| | ナビゲーションバー (静的)                 | |
| +-------------------------------------------+ |
| |                                           | |
| | 商品画像、商品名、商品説明 (静的)         | |
| |                                           | |
| | +---------------------------------------+ | |
| | | 動的ホール (1): レビュー (プレースホルダー)| |
| | +---------------------------------------+ | |
| |                                           | |
| | +---------------------------------------+ | |
| | | 動的ホール (2): おすすめ商品 (プレースホルダー) | |
| | +---------------------------------------+ | |
| |                                           | |
| +-------------------------------------------+ |
| | フッター (静的)                           | |
| +-------------------------------------------+ |
+-------------------------------------------------+
</code></pre><h3 id="pprの動作フロー">PPRの動作フロー</h3>
<p>ユーザーがページにアクセスした際の動作フローは以下のようになります。</p>
<ol>
<li><strong>即時レスポンス:</strong> ユーザーからのリクエストに対し、CDNにキャッシュされている「静的シェル」が即座に返されます。これにより、TTFBが劇的に改善され、ユーザーはすぐにページの骨格を見ることができます。</li>
<li><strong>動的コンテンツのストリーミング:</strong> ブラウザが静的シェルを表示している間に、Next.jsサーバーは「動的ホール」の中身を非同期でレンダリングします。</li>
<li><strong>コンテンツの注入:</strong> レンダリングが完了した動的コンテンツは、HTTPストリーミングを通じてブラウザに送信され、対応するプレースホルダー部分にシームレスに注入されます。この間、<code>Suspense</code>の<code>fallback</code>に指定したスケルトンUIやローディングスピナーが表示されるため、ユーザーはデータが読み込み中であることを認識できます。</li>
</ol>
<p>このアーキテクチャにより、ユーザーはサーバーでの重い処理が終わるのを待つことなく、インタラクション可能なページを即座に手に入れることができるのです。</p>
<h3 id="コードで見るpprsuspenseが鍵">コードで見るPPR：<code>Suspense</code>が鍵</h3>
<p>驚くべきことに、PPRを有効にするために特別な設定はほとんど必要ありません。Next.js 14.1以降のApp Routerを使用していれば、<strong>PPRはデフォルトで有効</strong>です。</p>
<p>PPRの魔法を実現する鍵となるのが、Reactの <strong><code>&lt;Suspense&gt;</code></strong> コンポーネントです。<code>&lt;Suspense&gt;</code>でラップされたコンポーネントが「動的ホール」として扱われます。</p>
<p>それでは、Eコマースの商品ページを例に、具体的なコードを見ていきましょう。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">22
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">23
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">24
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">25
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">26
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">27
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">28
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">29
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">30
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">31
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">32
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">33
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">34
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">35
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">36
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">37
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">38
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">39
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">40
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">41
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">42
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">43
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">44
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">45
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">46
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">47
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">48
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">49
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">50
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">51
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">52
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">53
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">54
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">55
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">56
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">57
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">58
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">59
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-jsx" data-lang="jsx"><span style="display:flex;"><span><span style="color:#75715e">// app/products/[id]/page.tsx
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Suspense</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;react&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#a6e22e">ProductDetails</span> <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/components/ProductDetails&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Reviews</span>, <span style="color:#a6e22e">ReviewsSkeleton</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/components/Reviews&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Recommendations</span>, <span style="color:#a6e22e">RecommendationsSkeleton</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/components/Recommendations&#39;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">getStaticProductData</span> } <span style="color:#a6e22e">from</span> <span style="color:#e6db74">&#39;@/lib/products&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// このページ自体は静的として扱われる
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">default</span> <span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">ProductPage</span>({ <span style="color:#a6e22e">params</span> }<span style="color:#f92672">:</span> { <span style="color:#a6e22e">params</span><span style="color:#f92672">:</span> { <span style="color:#a6e22e">id</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">string</span> } }) {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// 商品の基本情報（商品名、説明など）はビルド時に取得される
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#75715e">// このデータ取得はキャッシュされる
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">product</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">getStaticProductData</span>(<span style="color:#a6e22e">params</span>.<span style="color:#a6e22e">id</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> (
</span></span><span style="display:flex;"><span>    &lt;<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* ============================= */</span>}
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* === ここからが静的シェル === */</span>}
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* ============================= */</span>}
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">header</span>&gt;<span style="color:#a6e22e">My</span> <span style="color:#a6e22e">E</span><span style="color:#f92672">-</span><span style="color:#a6e22e">Commerce</span> <span style="color:#a6e22e">Site</span>&lt;/<span style="color:#f92672">header</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">main</span>&gt;
</span></span><span style="display:flex;"><span>        {<span style="color:#75715e">/* このコンポーネントは静的にレンダリングされる */</span>}
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">ProductDetails</span> <span style="color:#a6e22e">product</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">product</span>} /&gt;
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">hr</span> /&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        {<span style="color:#75715e">/* ========================================= */</span>}
</span></span><span style="display:flex;"><span>        {<span style="color:#75715e">/* === ここからが動的なホール (1): レビュー === */</span>}
</span></span><span style="display:flex;"><span>        {<span style="color:#75715e">/* ========================================= */</span>}
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">Suspense</span> <span style="color:#a6e22e">fallback</span><span style="color:#f92672">=</span>{&lt;<span style="color:#f92672">ReviewsSkeleton</span> /&gt;}&gt;
</span></span><span style="display:flex;"><span>          {<span style="color:#75715e">/* 
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            Reviewsコンポーネントは内部で動的なデータ取得を行う。
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            (例: fetch(..., { cache: &#39;no-store&#39; }))
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            このため、この部分はリクエスト時にレンダリングされる。
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          */</span>}
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#f92672">Reviews</span> <span style="color:#a6e22e">productId</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">product</span>.<span style="color:#a6e22e">id</span>} /&gt;
</span></span><span style="display:flex;"><span>        &lt;/<span style="color:#f92672">Suspense</span>&gt;
</span></span><span style="display:flex;"><span>        
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">hr</span> /&gt;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        {<span style="color:#75715e">/* =================================================== */</span>}
</span></span><span style="display:flex;"><span>        {<span style="color:#75715e">/* === ここからが動的なホール (2): おすすめ商品 === */</span>}
</span></span><span style="display:flex;"><span>        {<span style="color:#75715e">/* =================================================== */</span>}
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">h2</span>&gt;<span style="color:#a6e22e">あなたへのおすすめ</span>&lt;/<span style="color:#f92672">h2</span>&gt;
</span></span><span style="display:flex;"><span>        &lt;<span style="color:#f92672">Suspense</span> <span style="color:#a6e22e">fallback</span><span style="color:#f92672">=</span>{&lt;<span style="color:#f92672">RecommendationsSkeleton</span> /&gt;}&gt;
</span></span><span style="display:flex;"><span>          {<span style="color:#75715e">/* 
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            Recommendationsコンポーネントも動的なデータ取得を行う。
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">            レビューとは独立してストリーミングされる。
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">          */</span>}
</span></span><span style="display:flex;"><span>          &lt;<span style="color:#f92672">Recommendations</span> <span style="color:#a6e22e">category</span><span style="color:#f92672">=</span>{<span style="color:#a6e22e">product</span>.<span style="color:#a6e22e">category</span>} /&gt;
</span></span><span style="display:flex;"><span>        &lt;/<span style="color:#f92672">Suspense</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;/<span style="color:#f92672">main</span>&gt;
</span></span><span style="display:flex;"><span>      &lt;<span style="color:#f92672">footer</span>&gt;<span style="color:#a6e22e">Footer</span> <span style="color:#a6e22e">Content</span>&lt;/<span style="color:#f92672">footer</span>&gt;
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* =========================== */</span>}
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* === ここまでが静的シェル === */</span>}
</span></span><span style="display:flex;"><span>      {<span style="color:#75715e">/* =========================== */</span>}
</span></span><span style="display:flex;"><span>    &lt;/<span style="color:#f92672">div</span>&gt;
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>このコードのポイントは以下の通りです。</p>
<ol>
<li><strong>ページの基本構造は静的:</strong> <code>ProductPage</code>自体は<code>async</code>コンポーネントですが、内部で行われるデータ取得 (<code>getStaticProductData</code>) がキャッシュされるため、ページの主要部分は静的に事前生成されます。これが「静的シェル」となります。</li>
<li><strong><code>Suspense</code>で動的境界を定義:</strong> <code>&lt;Reviews&gt;</code>と<code>&lt;Recommendations&gt;</code>コンポーネントは、それぞれ<code>&lt;Suspense&gt;</code>でラップされています。これらのコンポーネントは、内部で動的なデータフェッチ（例えば <code>fetch</code> の <code>cache: 'no-store'</code> オプションや、<code>cookies()</code>、<code>headers()</code> といったDynamic Functionsの使用）を行うため、「動的ホール」として扱われます。</li>
<li><strong><code>fallback</code>でUXを向上:</strong> 動的コンテンツがロードされるまでの間、<code>fallback</code>プロパティに指定されたスケルトンコンポーネント (<code>&lt;ReviewsSkeleton /&gt;</code>, <code>&lt;RecommendationsSkeleton /&gt;</code>) が表示されます。これにより、レイアウトシフトを防ぎ、ユーザー体験を向上させます。</li>
</ol>
<p>このように、開発者は「どこを動的にしたいか」を<code>&lt;Suspense&gt;</code>で宣言するだけで、Next.jsが自動的にPPRを適用し、最適なレンダリングを行ってくれるのです。</p>
<h2 id="pprがもたらすメリットと考慮すべき点">PPRがもたらすメリットと考慮すべき点</h2>
<p>PPRは多くのメリットをもたらしますが、採用する上で知っておくべき考慮点も存在します。</p>
<h3 id="メリット">メリット</h3>
<ol>
<li>
<p><strong>圧倒的なパフォーマンス向上:</strong></p>
<ul>
<li><strong>TTFB (Time to First Byte) の劇的な改善:</strong> CDNから静的シェルを即座に返せるため、サーバーでのレンダリング待ち時間がなくなり、TTFBは限りなくゼロに近づきます。</li>
<li><strong>FCP/LCP の高速化:</strong> ユーザーはページの主要なコンテンツをすぐに視認できるため、First Contentful Paint (FCP) や Largest Contentful Paint (LCP) といったCore Web Vitalsの指標が大幅に改善されます。</li>
</ul>
</li>
<li>
<p><strong>最高のユーザー体験 (UX):</strong></p>
<ul>
<li>静的な部分は即座に表示され、インタラクション可能になります。</li>
<li>動的な部分はスケルトンUIなどで読み込み中であることが明示されるため、ユーザーは白紙の画面を見て待たされるストレスから解放されます。</li>
</ul>
</li>
<li>
<p><strong>優れた開発体験 (DX):</strong></p>
<ul>
<li>「SSGかSSRか」というページ単位での二者択一の悩みから解放されます。</li>
<li>開発者はコンポーネント単位でキャッシュ戦略を考えるだけでよくなり、より直感的にアプリケーションを構築できます。</li>
<li>複雑な設定は不要で、Reactの標準的な機能である<code>&lt;Suspense&gt;</code>を使うだけで実現できるシンプルさも魅力です。</li>
</ul>
</li>
<li>
<p><strong>インフラコストの削減:</strong></p>
<ul>
<li>静的シェルがCDNから配信されることで、オリジンサーバーへのリクエスト数が減少します。これにより、サーバーの負荷が軽減され、インフラコストの削減につながる可能性があります。</li>
</ul>
</li>
</ol>
<h3 id="デメリット--考慮すべき点">デメリット / 考慮すべき点</h3>
<ol>
<li>
<p><strong>App RouterとRSCへの依存:</strong>
PPRは、React Server Components (RSC) を前提としたApp Routerのアーキテクチャ上で成り立っています。従来のPages Routerでは利用できず、PPRの恩恵を受けるためにはApp Routerへの移行が必要です。</p>
</li>
<li>
<p><strong><code>Suspense</code>の境界設計の重要性:</strong>
<code>Suspense</code>の境界（boundary）をどこに設定するかが、パフォーマンスとUXを左右します。境界が大きすぎるとストリーミングのメリットが薄れ、細かすぎるとコンポーネントの管理が複雑になる可能性があります。戦略的な設計が求められます。</p>
</li>
<li>
<p><strong>キャッシュ戦略の理解:</strong>
ページ単位ではなく、コンポーネントやデータフェッチ単位でキャッシュを管理する必要があるため、アプリケーション全体のキャッシュ戦略をより深く理解し、設計することが重要になります。特に、意図せずページ全体が動的レンダリングになってしまうケース（ルートレイアウトでのDynamic Functionsの使用など）には注意が必要です。</p>
</li>
<li>
<p><strong>サードパーティライブラリの互換性:</strong>
RSCに対応していないクライアントサイド専用のライブラリ（例: <code>window</code>オブジェクトに直接アクセスするもの）は、<code>'use client'</code>ディレクティブを付けたClient Component内で使用する必要があります。PPRの文脈でもこの原則は変わらないため、ライブラリの選定や利用方法に注意が必要です。</p>
</li>
</ol>
<h2 id="現場で使えるppr実践tips">現場で使えるPPR実践Tips</h2>
<p>PPRを最大限に活用するための、より実践的なヒントをいくつかご紹介します。</p>
<h3 id="tip-1-suspenseの境界を戦略的に設計する">Tip 1: <code>Suspense</code>の境界を戦略的に設計する</h3>
<p><code>Suspense</code>でどこを囲むかは非常に重要です。以下の点を考慮して設計しましょう。</p>
<ul>
<li><strong>Above the Fold (ファーストビュー) は静的に:</strong> ユーザーが最初に目にする画面（スクロールせずに見える範囲）は、可能な限り静的シェルに含め、即座に表示されるようにします。LCPの改善に直結します。</li>
<li><strong>Below the Fold (スクロール後) を動的に:</strong> コメント欄や関連商品リストなど、スクロールしないと見えない部分は、積極的に<code>Suspense</code>で囲み、遅延ロードさせましょう。これにより、初期表示のパフォーマンスへの影響を最小限に抑えられます。</li>
<li><strong>データ取得が遅いコンポーネントを分離する:</strong> 外部APIへの依存などでどうしても読み込みが遅くなるコンポーネントは、他のUIから独立させて<code>Suspense</code>で囲むことで、そのコンポーネントの遅延がページ全体の表示をブロックするのを防ぎます。</li>
</ul>
<h3 id="tip-2-高品質なスケルトンuiを実装する">Tip 2: 高品質なスケルトンUIを実装する</h3>
<p><code>fallback</code>に表示されるUIは、ユーザー体験を大きく左右します。</p>
<ul>
<li><strong>単なる「ローディング中&hellip;」は避ける:</strong> テキストだけのローディング表示は、味気なく、ユーザーに不安を与える可能性があります。</li>
<li><strong>レイアウトを維持するスケルトンを:</strong> 実際に表示されるコンテンツのレイアウトを模したスケルトンUI（Shimmer UIとも呼ばれる）を用意しましょう。これにより、コンテンツが読み込まれた際のレイアウトシフト（CLS: Cumulative Layout Shift）を防ぎ、スムーズな表示を実現できます。<code>shadcn/ui</code>などのライブラリには、便利な<code>Skeleton</code>コンポーネントが用意されています。</li>
</ul>
<h3 id="tip-3-nextjsのキャッシュとレンダリングを可視化デバッグする">Tip 3: Next.jsのキャッシュとレンダリングを可視化・デバッグする</h3>
<p>意図通りにPPRが機能しているかを確認することが重要です。</p>
<ul>
<li><strong>ビルドログの確認:</strong> <code>next build</code> を実行すると、各ページが静的（λ）か動的（○）かが出力されます。PPRが適用されているページは静的（λ）としてマークされるはずです。</li>
<li><strong>ブラウザの開発者ツール:</strong> Networkタブでドキュメントのレスポンスを確認します。PPRが機能している場合、最初のHTMLレスポンスには静的シェルと<code>Suspense</code>の<code>fallback</code>が含まれ、その後、動的コンテンツがチャンクとしてストリーミングされてくる様子を観察できます。</li>
<li><strong>Vercelへのデプロイ:</strong> Vercelにデプロイすると、Speed InsightsやAnalytics機能を使って、TTFBやLCPといったパフォーマンス指標を実環境でモニタリングし、PPR導入による改善効果を定量的に測定できます。</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>Next.js 16で安定版となったPartial Prerendering (PPR)は、単なる新機能ではありません。それは、Webアプリケーションのパフォーマンスと体験を根底から変える、<strong>レンダリングの新しいパラダイム</strong>です。</p>
<p>これまで私たちが直面してきた、静的サイトの「速度」と動的アプリケーションの「柔軟性」のトレードオフ、すなわち「All or Nothing」問題は、PPRによってついに解決されました。PPRは、React Server Componentsと<code>Suspense</code>を組み合わせることで、1つのページ内に静的なシェルと動的なホールを共存させ、両者の利点を同時に享受することを可能にします。</p>
<p>その結果として得られるのは、CDNエッジからの即時レスポンスによる驚異的なTTFB、最適化されたCore Web Vitals、そしてシームレスでストレスのないユーザー体験です。開発者にとっても、複雑なレンダリング戦略の選択に頭を悩ませることなく、コンポーネント単位でその性質を考えるだけで、フレームワークが最適なパフォーマンスを引き出してくれるという、この上ない開発体験を提供します。</p>
<p>App Routerへの移行というハードルはありますが、PPRがもたらす恩恵はそれを補って余りあるものです。今後のWeb開発において、このアーキテクチャがスタンダードになっていくことは間違いないでしょう。</p>
<p>さあ、あなたのNext.jsプロジェクトでも<code>&lt;Suspense&gt;</code>を使って、遅延がちなコンポーネントをラップすることから始めてみませんか？ 小さな一歩からでも、PPRがもたらす次世代のWebパフォーマンスをきっと実感できるはずです。Next.js 16と共に、より速く、より快適なWebの世界を構築していきましょう。</p>
]]></content:encoded>
      <category>Frontend</category>
      <category>Next.js</category>
      <category>PPR</category>
      <category>Web</category>
    </item>
  </channel>
</rss>
