🎈

App Routerでレンダリングの種類はどう変わった?

2024/07/25に公開

App Routerと言う新しいアーキテクチャが生まれてから、既存のPage RouterにあったSSR-SSG-ISRはApp Routerではどうすればいいのか?といった疑問が出てくると思います。
実際、これに関する記事は多く世の中に公開されています。
例えば、fetchでno-store, force-staticをcacheに設定することや、ページ単位で変数をエクスポートするなどがあります。しかし、私はこの単位で見たときに非常にややこしく見えました。

そもそも、App Routerのドキュメントからはこの枠組みが消えて、 Static Rendering - Dynamic Rendering, Streaming, Partial Prerenderingで解説されています。

実際に、App RouterのチュートリアルやドキュメントのServer Componentのレンダリングに関する部分でもこの分類が使用されています。

https://nextjs.org/docs/app/building-your-application/rendering/server-components

https://nextjs.org/learn/dashboard-app/static-and-dynamic-rendering

本稿ではこの分類は何を指しているのか、どんな状況で使うのかを私なりに考えたので、解説していきます。
また、App Router(v14)のデフォルトの挙動である、キャッシュできるものは可能な限りする方針でいきます。

全体像

先に、各種レンダリングはいつ使うのか?に関する全体像から述べます。
まず、これらのレンダリングはデータフェッチとの結びつきが強く、その種類によって異なる挙動をします。

sumirenさんのデータフェッチの頻度やタイミングで名前をつけるのが非常に素晴らしいアイデアだと思い、紹介されているものと微妙に異なるのですが、本記事にあった用語を定義します。

https://zenn.dev/sumiren/articles/349c60f19c505f#用語体系(素案)

  • 再検証付きデータフェッチ
    • デフォルトでキャッシュされ、必要な時だけ再検証する(ブログや商品などの詳細データ)
  • ダイナミックデータフェッチ
    • ユーザー固有のデータといったキャッシュされないデータ(おすすめやログイン中のユーザーなどのデータ)
    • 変更タイミングが検知できない、または高頻度に内容が変わる外部のAPIなど

そうすると、各種のレンダリングは以下のように分類できます。

種類 条件 タイミング 備考
Static Rendering 再検証付きデータフェッチのみ ビルド・再検証時 事前にHTMLとRSC Payloadを作るので高速
Dynamic Rendering ダイナミックデータフェッチ, dynamic functionを含む リクエスト時 再検証付きデータフェッチは含んでもOK
Streaming Dynamic Renderingのダイナミックデータフェッチの箇所をSuspenseで分ける リクエスト時 遅いダイナミックデータフェッチのレスポンスを遅延させる
Partial Prerendering 再検証付きデータフェッチとダイナミックデータフェッチをSuspense境界で分ける ビルド・再検証・リクエスト時 前者をStatic Rendering, 後者をStreamingする

これらを踏まえて、各種レンダリングの話をしていきます。

その前にFull Route Cacheの話

Static RenderingとDynamic Renderingは、Full Route Cacheとの関わりがあるので軽く触れておきます。

Full Route Cacheはルート単位でHTML, RSC Payloadサーバー側にキャッシュします。
例えば、/about, /articlesみたいなパス単位でキャッシュをすることができます。

キャッシュの更新タイミングはビルド時・再検証(revalidate)時であり、任意のユーザーで共有されるタイプのキャッシュとなっています。

これだけ見ると以下のような疑問が湧くと思います。

  • ユーザー固有のデータがあった場合は?
  • 記事やプロダクトの詳細データの取得をする必要があるページは?

これらの疑問とStatic Rendering - Dynamic Renderingは関係しているので、次の章で解説します。

レンダリング別の解説

App Router(v14)では、ビルド時にルート単位でStatic RenderingとDynamic Renderingに分類されます。

本章では各種レンダリングの違いを述べます。

Static Renderingとは

Static Renderingとは、各ルートがビルド時 or 再検証時にレンダリングされます。
開発者が特定のコードを入れない限りは、デフォルトでStatic Renderingになるのも特徴です。
ここからApp Routerがパフォーマンスを向上させようとする意思を感じますね。

レンダリングではRSC PayloadHTMLを生成しており、これはFull Route Cacheで実現[1]しています。
生成されたRSC PayloadHTMLは、私の環境では.next/server/appの中にありました。

このレンダリングを採用するページ候補は、ユーザ固有のデータを含まない、かつ記事やプロダクト詳細ページなどのビルド時に取得可能なデータを含むページです。
データフェッチの種類だと、再検証付きデータフェッチとなります。

再検証は通常の時間をベースにしたものと、オンデマンドの2種類あります。
後者のオンデマンドはrevalidatePathrevaliteTagを指しています。
これを正しく扱うことができれば、変更時のみデータを再検証してパフォーマンスを向上しつつ、HTMLのレスポンスが可能になります。

一見Static Renderingはビルド時に生成して、その後は変えられないように感じます。
しかし、再検証は任意のタイミングで行えるため、変更があったタイミングで再検証を走らせて整合性をとることが可能です。これを完璧に制御しようとすると、現段階ではだいぶ複雑になりそうなのが難点ですが。。

Dynamic Renderingとは

逆にDynamic Renderingでは、各ルートがリクエスト単位でレンダリングします。
これは既存のSSRと同じイメージで、Full Route Cacheされません。
データフェッチの種類だと、ダイナミックデータフェッチを使っているページに当たります。

主に使用されるページは、ユーザー固有のデータや、cookieやHTTP Header, URLのクエリといったデータを扱うところが想定されます。

デフォルトでStatic Renderingなのを、Dynamic Renderingに変える要因は以下があります。

  • cookies, headers, searchParamsといったdynamic functionをページ内のどこかで使う
  • ページ内のどこかでno-storeなどキャッシュしない設定のfetchを使う
  • dynamic = 'force-dynamic'やrevalidate = 0を設定する

ここで、再検証付きデータフェッチはDynamic Renderingでもそのままキャッシュ可能です。
なので、理想は再検証付きデータフェッチはキャッシュで、ダイナミックフェッチはキャッシュしないという方針になります。

Streamingとは

Streamingはデータをチャンクに分けて配信するもので、Dynamic Renderingの一部状況で使われるレンダリング手法です。

より具体的にはSuspense境界の中にダイナミックデータフェッチを入れて、完了してから中身をレスポンスします。境界の外側はDynamic Rendering後が完了後、先にレスポンスされます。この時、Suspenseのfallbackが表示されます。

Streamingの嬉しい点は、遅いダイナミックデータフェッチに依存してTTFBが悪くなることを防げることです。高速なダイナミックフェッチや再検証付きデータフェッチで、先に元となるHTMLをレンダリングして配信することが可能になります。

Partial Prerenderingとは

Partial Prerendaring(以下PPR)はApp Router (v14)で実験的ですが、チュートリアル[2]でも触れられていますし、今後のApp Routerの大きな目玉になりそうなので解説します。

PPRは、一言で言えばStatic RenderingとStreamingの合わせ技です。

世の中のページは、一部分がStatic Rendering, 一部分がDynamic Renderingに当たるなど、完璧にどちらかの性質だけを持ったページというのは多くありません。
例えば、記事本体の部分はStaticだが、おすすめはDynamicであるといった感じです。

しかし、上記の分類だとページ内にダイナミックデータフェッチがあるだけで、全体がDynamic Renderingになり、Full Route Cacheされないといった問題点がありました。

そこで、新たにPPRという新しいレンダリング手法が提案されました。
PPRでは、Static RenderingとDynamic Renderingのいいとこ取りをしています。
つまり、Static Renderingが可能な箇所は事前にFull Route Cacheをしておき、リクエスト時に必要最小限なDynamic RenderingをしてStreamingで返すことができます。

実装では、この境界をStreamingと同じくSuspenseで決定します。

一見複雑そうに見えるPPRですが、これらを踏まえてみると自然な最適化のように思えました。
公式でも以下のように述べられており、今後のデフォルトのレンダリングモデルになる可能性を秘めています。

We believe PPR has the potential to become the default rendering model for web applications

まとめ

App Routerで述べられているレンダリングの種類と関係性について私なりにまとめて書いてみました。

App Routerは結構意見が分かれるところであると思いますが、私はパフォーマンスに全力で取り組むようになったフレームワークと考えています。
今まで通りの簡単に使えるフレームワークが欲しいと考えていると、思ってたのと違うとなりそうです。
今後は要件に沿った他のフレームワークとの検討が重要になってきそうです。

ここまで読んでいただきありがとうございました!
間違っている箇所への指摘は大歓迎ですので、お気軽にコメントをお願いします。

脚注
  1. https://nextjs.org/docs/app/building-your-application/caching#full-route-cache ↩︎

  2. https://nextjs.org/learn/dashboard-app/partial-prerendering ↩︎

Discussion