Next.js 15ではルーティングとキャッシュの制御が変わった
そんなApp Routerの次の時代を切り開くNext.js 15が2024年10月21日に発表され、続いて、同年12月10日にはNext.js 15.1が発表されました。
開発体験を向上する魅力的な改善が多く見られますが、特に注目すべきはルーティングとキャッシュに関する破壊的な変更です。
- リクエスト内容の参照に関するAPIを非同期にする(Async Request APIs)
- いくつかの挙動でデフォルトをキャッシュなしにする(Caching Semantics)
破壊的な変更ということで、これまでのApp Routerを使っていたユーザーにとっては心配もあるかと思いますが、 @next/codemodという更新用のCLIが整備されているので、リリースノートを読みながら活用してみてください。
リクエスト内容の参照に関するAPIを非同期にする
まずは、Server Componentの内部でリクエスト内容を参照するAPIに関する変更です。これは開発体験の向上というより、フレームワーク側の速度向上や最適化を目的とした変更です。変更の対象となるAPIはいくつかあり、関数として提供されるものは次の3つです。
-
cookies -
headers -
draftMode
そして、propsや引数として提供されるものは次の2つです。
-
params(layout.js、page.js、route.js、default.js、generateMetadata、generateViewport) -
searchParams(page.jsのみ)
これらの変更後のAPIを利用すると、page.jsはリスト1の書き方になります。
import { headers } from "next/headers";
export default async function AsyncRequestApisPage(props) {
const _headers = await headers(); // (1)
const params = await props.params; // (2)
const searchParams = await props.searchParams; // (3)
return (
<main className="container p-10">
<h1 className="text-2xl font-bold mb-4">Async Request APIs</h1>
<p>Params: {JSON.stringify(params)}</p>
<p>Search Params: {JSON.stringify(searchParams)}</p>
<p>Headers: {JSON.stringify(_headers)}</p>
</main>
)
}
リスト1を実際に手元で動かして、http://localhost:3000/async-request-apis/foobar?query=buz にアクセスすると、図1のような表示になります。
無事に動作していそうです。(1)は従来はオブジェクトを返す関数でしたが、今回の変更でPromiseを返す関数に変更され、呼び出しには await が必要になりました。また、(2)や(3)のようにURLのパスに含まれるパラメータやSearch Paramsを取得するAPIも、Promiseを返すように変更されています。propsから取り出した値に await を付けるのは、慣れるまでは少し奇妙に感じられそうですね。
なぜこのような変更が行われたのか見てみましょう。従来はheaders()やprops.params などを同期的に扱うために、リクエストの到達を待ってからpage.jsなどに紐づくServer Componentの評価を始めていました。しかしながら、リクエスト依存のパラメータを使わなくても評価できるページやレイアウトも多いため、リクエストの到達を待つ時間がオーバーヘッドになっていたようです。
今回の変更により、リクエストの到達を待たずにServer Componentの評価を始めることができるようになり、たまたまcookiesやsearchParamsなどを使っていたページだけが評価に少し時間がかかる、といった世界観に変わりました。
awaitを書く場所が増える点では開発体験は少し悪くなるのですが、Server Componentsを扱い慣れてきたNext.jsチームならではの最適化だと思うので、筆者は好意的に捉えています。
いくつかの挙動でデフォルトをキャッシュなしにする
さて、もうひとつの破壊的な変更として、いくつかの挙動においてデフォルトでキャッシュしていた挙動がキャッシュしないように変更されたものがあります。ユーザーからのフィードバックとして、キャッシュはオプトインで実装されていたほうが挙動を予測しやすい、という意見があったのではないかと、筆者は推測しています。
まずは、Route Handler(route.js)で定義しているGET関数のレスポンスを、原則としてキャッシュしないようにする変更です。Next.js 15からは原則として毎回評価されます。export dynamic = 'force-static'を記載することでオプトインでキャッシュを有効にすると、従来通りの挙動になるようです。
次に、Client Routerの挙動の変更です。Next.js 14.2.0でnext.config.jsにstaleTimesという設定が追加され、動的なコンテンツを含むページ遷移であっても、所定の時間が経過するまではキャッシュを使ってページ遷移を行うようになっていました。デフォルトで30秒間キャッシュされるようになっていましたが、今回からはデフォルトで0秒が設定され、キャッシュを使わず毎回動的にコンテンツを取得するようになりました。
従来通りの挙動にするためには、リスト2のようにstaleTimesを設定する必要があります。
const nextConfig = {
experimental: {
staleTimes: {
dynamic: 30,
},
},
};
export default nextConfig;
そして最後に、リリースノートの記事に書かれていない変更点として、fetch()関数のデフォルトのキャッシュの挙動が変更されています。公式ドキュメントの「Data Fetching and Caching」のページを見てみましょう。
次のような記載があります。
> Good to know:
>
> In previous versions of Next.js, using
fetchwould have a defaultcachevalue offorce-cache. This changed in version 15, to a default ofcache: no-store.
従来はServer Componentを動的レンダリングする際にfetch()関数を実行すると、第一引数のURL文字列をキーにして自動でキャッシュされる挙動になっていましたが、今後はデフォルトでキャッシュしない挙動になります。
キャッシュしたほうが都合がいい場合もあると思うので、その場合はリスト3のようにcache: 'force-cache'を指定してください。
export default async function FetchOptIn() {
const text = await fetch(
// base64 エンコードした "App Router is AWESOME" という文字列をデコードする
"https://httpbin.org/base64/QXBwIFJvdXRlciBpcyBBV0VTT01F",
{
cache: "force-cache", // (1)
next: {
tags: ["my-base64"] // (2)
}
}
).then((res) => res.text());
return (
<main className="container p-10">
<h1 className="text-2xl font-bold mb-4">Fetch OptIn</h1>
<p>text: {text}</p>
</main>
)
}
(1)のように指定することで、従来通りの挙動にすることができます。(2)のようにタグをつけて、Server ActionsなどのタイミングでrevalidateTag()でキャッシュを更新する流れは特に変わりません。
このように、Next.js 15では、暗黙的にキャッシュしていた挙動のいくつかが廃止され、明示的に指定する形に変更されました。キャッシュを有効にする箇所は少なくないと思われるので、開発者が手を動かす量は増えてしまったのかもしれませんが、それでも暗黙的な挙動でトラブルが発生する心労と比べれば、オプトイン方式のほうが安全であるという意見に筆者は同意します。
