LT:App Router 悲喜交々
今日はv15がテーマですが、どちらかと言うと、v13、v14の話をあっきーさんの前座としてお話しできればと思います。
今日はそんなに長くないのでかいつまんでとはなりますが、今年の4月にも同じ案件でApp Router使ってどうだったかの話をしているので、もしより詳細が気になる方は資料を参考にしてもらえればと思います。
アジェンダとしては、まずはApp Router開発で辛かったこと、その上でApp Routerを採用してよかったことの2本立てでお話しようと思います。
App Routerで苦しんだこと
早速苦しんだこと・辛かったことです。
白い表は、Next.jsのApp Routerにさまざまなキャッシュがあるという話と関連しています。
Pages Routerを思い返すと、こんなに多くのキャッシュはありませんでした。App Routerになってからキャッシュが増え、それに対する向き合い方にかなり苦労した記憶があります。
例えば4つあるうちの真ん中の2つは(Data Cache、Full Route Cache)サーバサイドでPersistent(永続化)されるデータのキャッシュと、HTMLやRSC Payloadの画面のキャッシュのようなものです。
自分の案件でも、キャッシュのヒットレートが上がらなさそうだと感じ、一度作ってもほとんど参照されないこともわかっていました。むしろ、表示してはいけないタイミングで表示されて障害になるリスクの方が大きいため、意図的にオプトアウトしていました。
もう一つ、一番下のRouter Cache だけ、場所がブラウザになるin-memoryキャッシュです。以前自分のブラウザが表示した画面を覚えていて、それを高速に切り替えるイメージです。これはオプトアウトできずに、サーバアクションから殺せる関数を読んで殺す手段が与えられています。
そのような手段を使って注意深く殺していきました。
あっきーさんのこの後のお話で詳しく触れられると思いますが、キャッシュのデフォルトの挙動がv15で大きく変わるため、キャッシュを殺すために苦労した人にとっては、嬉しい変更になるかもしれません。
オブザーバビリティ系のツールと連携しようとしたら、期待した挙動にならないこともありました。また、さきほどお話ししたRouter Cache があるはずなのにない状態で「ドキュメントと違う挙動をしている」ケースやNext.jsの機能でIntercepting RoutsやParallel Routesなど凝った画面……1個のレイアウトの中に複数のルートのポイントが入っているようなパターンのUIでは細かい不具合がいくつかありました。
ここに書いているものは、今試すと大体修正されているものばかりですが、早めにプロダクションを入れようと思ったばかりにこら辺で苦しんだ事実がありました。
それでもApp Routerをやっていてよかったと思ったことがあります。
App Router 採用してよかったと感じたこと
Pages Routerと比較してもServer Componentsに直接Fetchや極端にはDBの参照みたいなものが書けますし、Server Actionsを使えばタイプスクリプトのコードを書いているように非同期にデータを更新することも可能です。
Data Fetchや取得更新まわりが気持ちよく、自分のやりたいことがそのままコードに落とせる感じがとても気に入っています。
それらを使ってデータの取得をするのがNext Wayで、これらはどれもページコンポーネントに紐づいて書くstatic関数なんです。
あるページのツリーが必要とするデータはその手前のページのgetServerSidePropsやGIPで取得して、そこで取得したものを下のプロパティに流し込んでいくことしかできず、この部分が嫌いでした。
Componentも必要なデータを取ってくることがRSCによって実現できるようになりました。
これが先ほど言った「自律的な」という心のような部分で好きなところです。
Nextとは関係なくGraphQLをずっと使っていますが、GraphQLもこれと同じ属性が備わっているので、GraphQL好きにはものすごくうれしい部分です。
この辺りも好きなところで、自分のブログや動画のネタにもしているのでそちらも読んでいただければと思います。
これは先ほど紹介した案件をブラウザのパフォーマンス測定ツール「Lighthouse」を使って測った点数です。先ほど少し言った通りNextのキャッシュはあまり生かさず、Data CacheやFull Route Cacheを殺した状態で測定していますが、90点台という割と実用的な点数が出てくれています。
この点数だけではパッと分かりませんし、別のプロジェクトなのでフェアな比較ではないかもしれませんが、Pages Routerでやっているような案件と比較するとオーダーで違う程度にinitial loadのJavaScriptで差が出ます。
気を使って実装したわけではありませんが、素直にデフォルトのServer Componentの世界で実装するときちんと小さくなり、そこそこ良い点数が出るのだなと感じました。
フレームワークに従って実装するってすごいと思いました。
ahomu氏:ありがとうございました。
では続きまして、あっきーさんよろしくお願いいたします。
LT:Next.js v15.0.0-rc.0 キャッシュのデフォルト挙動改善とPPR
akfm氏:Next.jsのv15のRCがリリースされていますが、今回は初めに出たRC.0についてお話しします。今後、RC.1などがリリースされると思いますが、内容が変わる可能性があるため、今日はRC.0にフォーカスしてお話ししたいと思います。
v15.0.0- RC.0でサポートする話で進んでいましたが、Boomer Fetchingと呼ばれるReact19で他ライブラリとの使い方の兼ね合いが問題になってしまって、今React 19のリリースが止まっている状態です。
最終的に以降のRCでは「React 19を待たない」と変更されたので、v15がリリースされる際にはReact 19のサポートがなくなる可能性があります。代わりにキャナリーか何かのサポートが入るのかなと思っています。
使い勝手は変わりませんが、React19のメジャーリリースを待たずにv15が出ると予測ができます。
ややこしい話から始めてしまいましたが、その他の機能としては、複雑だったキャッシュ周りのデフォルトが一部変更になったり、PPR(Partial Pre Rendering)機能のオプションが用意されたり、next/afterというフックが用意されたりなどなどいろいろあります。
細かいことは抜きにして、今日僕が特にお話したいことはこの2点です。
・キャッシュのデフォルトを一部変更
・PPRのincrementalオプションが追加
この2つがv15のRC0では一番大きな注目すべきトピックだと思うので、ここら辺を今日は持って帰ってもらいたいなと思い、お話していきます。
cache
App Routerのキャッシュのお話はややこしいので、丁寧にお話しします。
一つ目が関数の戻り値としてフェッチなどをメモ化する「Request Memoization」についてです。これはメモ化なので、自動でメモされると思ってもらえればいいと思います。
2番目「Data Cache」はFetchの結果をサーバ側でキャッシュします。ひとつ目はメモ化なのでリロードするたびに破棄されますが、「Data Cache」はデータを共有するサーバ側のキャッシュになっています。
3番目が「Full Route Cache」です。これは従来からあるようなHTMLやApp Router、React Server ComponentsならではのRSC payloadというジェイソンやHTMLなどのフォーマットの一種のRSC payloadという独自のフォーマットのキャッシュです。
4番目がクライアントサイドのキャッシュ「Router Cache」です。
今回変更が入る「Data Cache」「Full Route Cache」「Router Cache」が、どのように変わるのかがミソとなっています。
「Data Cache」はデフォルトでキャッシュされる仕組みが実装者からパッと見えづらく、混乱の種となっていました。
「Full Route Cache」では、Pages Routerで従来のAPIツールのような「Router Handler」のGETを裁くハンドラーがキャッシュ可能で、デフォルトでキャッシュされる仕組みでした。そのため、ページやレイアウトもデフォルトでキャッシュされていました。
基本的に総じてキャッシュがデフォルトで強く有効になっていて、割と初見殺しで初学者に優しくない状態でした。
「Data Cache」もフェッチのデフォルトが"no-store"つまりキャッシュされない形に変更されました。
「Router Handler」でのGETもデフォルトでキャッシュされなくなり、基本的にキャッシュのデフォルト値が弱まりました。
初見殺しの部分が減ってきたところが、v15での変更の大きな点だと思っています。
簡単に言うと、デフォルトのキャッシュの挙動変更は去年の11月に始まっていましたが、どのようにパフォーマンスを損ねないで変更するかが問題でした。
Next.jsは哲学としてデフォルトで高いパフォーマンスを実現できることにすごく重きを置いているので、デフォルトのパフォーマンスを損ねるような変更はしたくないという判断があったのです。
これをこれから出てくるPPRが解決しました。
PPRがデフォルトになると、高いパフォーマンスを維持したままキャッシュのややこしい部分を減らす考えに至ります。PPRは現状エクスペリメンタルなのですが、実装もだいぶ進んできて、今後デフォルトになることが想定されます。
実装が進んで、設計の見直しも減ってきたので、Next.jsはPPRの方向に進んでいく方針が固まり、設計も固まり、デフォルトのキャッシュを変更してもいい段階に来たという判断があったのでこのタイミングでの変更となりました。
PPR
これを理解するには、まずApp RouterのStatic RenderingとDynamic Renderingという概念を理解する必要があります。
Static RenderingはSSGやISR相当で、ページを生成して必要ならrevalidateするものです。
Dynamic RenderingはSSR相当、リクエストごとにレンダリングできるような仕組みです。
厳密には少し違う部分もありますが、簡略化するとこういうことです。
そのためビルド時に生成したりしますが、カートやレコメンドはユーザーごとに違うUIパーツだと思います。これをサスペンスでくるむと、外側はStaticのまま部分的にDynamicなコンポーネントをレンダリングすることができます。これがPPRです。
今までは静的か動的か2択しかなかったのもが、2つをうまく組み合わせて「基本的には静的で部分的に動的」ができるようになりました。
現状この機能はエクスペリメンタルですが、パフォーマンスとシンプルな設計を両立できるため注目されています。また、僕らとしてもSuspenseでDynamic Renderingを簡単にくるむだけで済むので、とても嬉しい機能です。しかしNext.js側の実装はとても大変です。
PPRは、去年v14が発表されてから進んでいて、この1年ほどでバグ修正や内部テストが進められ、徐々に安定してきている段階です。
今までページ単位で設計を考えなければならなかったのが、Quramyさんもお話していたように、"コンポーネント思考"や"自律的"なコンポーネントごとに小さく実装できる、よりReactらしい設計が実現できる機能だと思います。
https://zenn.dev/akfm/articles/nextjs-partial-pre-rendering
まとめ
PPRの実装が進み先ほどのオプションが有効にできることになったことで、今まではPPRを有効にすると全ページで有効になっていたのが、V15からはこのフラグで「このページだけ有効」ということができるようになり段階的に導入することも可能になりました。
V15ではApp Routerがより初学者にやさしくシンプルな設計を実現できるようになると個人的に期待しています。
最後、直近の動きも話したかったのですが、時間もだいぶオーバーしてしまったのでこの辺でいったん切りたいと思います。
ありがとうございました。
ahomu氏:私自身v14のApp Routerと戯れた経験がありましたが、V15から入りたかったなという気持ちになってきました。
akfm氏:そう思えるような嬉しい変更がたくさんあるので、僕としてもV15から(そろそろ触ってみようかな)と思う方が増えたらいいなと思っています。
ahomu氏:改めてアッキーさんありがとうございました。
Xにも感想がたくさん来ていますので、運営で集約して質問などを拾っていきたいと思います。