【Next.js 15】cache実装が大幅改良される話
はじめに
今、Next.js 15がホットな話題ですが、そのNext.js 15で導入された、use cahce
により、これまでのNext.jsのcache戦略が大きく改良されることになりました。
また、よりシンプルかつ柔軟なcache戦略を取り入れやすくなり、大幅に開発体験が改良されることになるであろうため、use cache
について以下の記事を参考に解説しようと思います。
Next.js 14までの課題
fetch()
について
1. これまでNext.jsのfetch()
といえば、デフォルトでcacheされるものでした。
しかしながら、これはfetch()
を使用しない場合を、十分考慮したものではありませんでした。
そのため、ローカルデータベースアクセスに対する制御は十分に提供されていないといった課題がありました。
fetch()
以外を使用する場合、エスケープハッチが必要だった
2. エスケープハッチというのはハック的な方法で課題を解決することを言います。
これまでのNext.jsの場合、セグメントレベル(page.tsxやlayout.tsxなど)でのconfig設定やexport const dynamic
・runtime
・fetchCache
・dynamicparams
・revalidate =
など、何かしらの方法でハックするしかありませんでした。
※ Next.js 15でも、下位互換性のために、これらはサポートが続けられます
これらがNext.jsが難しいと言われている要因のほとんどで、これらを適切に理解するのですら大変なのです。
そこから、使いこなせるようになるのはもっと大変で、とにかく、cacheの最適化をするのが非常に難しいと言わざるを得ませんでした。
しかし、なんと、これからは、これらをすべて理解する必要がなくなります!
それに、以下の記事いわく、忘れて良いとのことです!
ということで、これからのcacheについて詳細に見ていこうと思います。
Next.js 15の場合
概要
まず、概要ですが、今後、cacheを使用する場合は、ページの先頭にuse cache
を宣言するだけでよくなります。
"use cache"
export default async function Page() {
return fetch(...)
}
詳細な変更点については、以下にまとめます。
変更点
fetch()
はデフォルトでcacheしなくなる
1. 上記、概要の通りで、use cahce
ディレクティブを使用して、cacheすることになるため、これまで、「fetch()
のデフォルトの挙動を変えるのはどうなの?」と言われていた問題が解決することになります。
これにより、fetch()
以外の方法も十分サポート可能となることが予想されます。
use cache
を付けると、静的ページを構成する
2. ページの先頭に これまではfetch()
を使用したコンポーネントを以下のように、Suspense
で囲む必要がありました。
async function Component() {
return fetch(...)
}
export default async function Page() {
return <Suspense fallback="..."><Component /></Suspense>
}
しかし、概要にもある通りで、今後はSuspense
が不要となります。
これは、取得した全データをcacheするためです。
"use cache"
export default async function Page() {
return fetch(...)
}
3. 部分的に静的コンテンツとすることが可能
まず、rootのlayout.tsxではuse cache
を宣言します。
そして、動的コンテンツとしたいものについてはuse cahce
を宣言しないことで実現できます。
なので、基本的な方針はSSG・SSRと変わらず、use cahce
を基本的に宣言することをデフォルトとして、動的コンテンツとしたい場合に宣言しないという運用になるかと思います。
"use cache"
export default async function Layout({ children }) {
const response = await fetch(...)
const data = await response.json()
return <html>
<body>
<div>{data.notice}</div>
{children}
</body>
</html>
}
import { Suspense } from 'react'
async function Component() {
return fetch(...)
}
export default async function Page() {
return <Suspense fallback="..."><Component /></Suspense>
}
4. 関数のcache
関数についてもcache可能で、ここでもcacheしたい関数にuse cache
を宣言するだけでよくなります。
また、関数のcacheについてはcache keyが不要が自動設定となるため、自分たちで設定・管理する必要がなくなります。
これは開発体験の向上に大きいと思います。
また、JSX内でawait
による関数呼び出しができるようにもなるようで、将来的には、このような記述が主流になるのかもしれません。
async function getNotice() {
"use cache"
const response = await fetch(...)
const data = await response.json()
return data.notice;
}
export default async function Layout({ children }) {
return <html>
<body>
<h1>{await getNotice()}</h1>
{children}
</body>
</html>
}
5. タグ付け
cache keyは不要になりますが、自分たちで管理する必要のあるcacheも存在することがあります。
その場合は、cacheTag()
にてcacheにタグ付けを行うことで管理することができます。
import { cacheTag } from 'next/cache';
async function getNotice() {
'use cache';
cacheTag('my-tag');
}
タグ付けしたcacheは従来のようにrevalidateTag()
をserver action
から呼び出してパージすることができます。
また、以下のように複数のタグを配列で単一のキャッシュエントリに割り当てることもできるようです。
cacheTag(['tag-one', 'tag-two'])
6. 特定のcacheのパージ
上記のタグ付けですが、具体的には、以下のようなケースで使用することが想定されているようです。
postのリストをfetchする関数でuse cache
を宣言すると、そのリストが更新されません。
ただし、以下のように識別子を用いて、タグ付けすることで、編集したデータについてのみ、更新を行うserver action
でrevalidateTag
を用いることでcacheをパージ・更新することができます。
import { unstable_cacheTag as cacheTag } from 'next/cache';
async function getBlogPosts(page) {
'use cache';
const posts = await fetchPosts(page);
for (let post of posts) {
cacheTag('blog-post-' + post.id);
}
return posts;
}
7. cahceの再検証
cacheLife()
により、cacheに有効期限をつけられ、任意のタイミングでrevalidateすることもできます。
やっていることとしては、ISRとほとんど同じで、さしづめ、ISRのcache版といった感じですね。
要するに、SWR(Stale-While-Revalidate)と同じで、revalidate(cache再検証)している間は古いcahceを返却するということができるということです。
※ SWR(Stale-While=Revalidate)についての詳細は以下の記事がわかりやすいと思います
CloudFrontで学ぶstale-while-revalidate、stale-if-errorディレクティブ
設定値としては、以下が設定できるようです。
- "seconds"
- "minutes"
- "hours"
- "days"
- "weeks"
- "max"
"use cache"
import { unstable_cacheLife as cacheLife } from 'next/cache'
export default async function Page() {
cacheLife("minutes")
return
}
ユースケースに最適な大まかな範囲を選択します。正確な数値を指定して、1 週間に何秒 (または何ミリ秒) があるかを計算する必要はありません。ただし、特定の値を指定したり、独自の名前付きキャッシュ プロファイルを構成したりすることもできます。
上記が仕様のようで、カスタム値も設定できるようです
ドキュメントにカスタム値の細かい設定方法が記載してありました。
next.config.js
でdynamicIO
フラグを有効にし、cacheLife
に対してオブジェクトを定義して使用するようです。
module.exports = {
experimental: {
dynamicIO: true,
cacheLife: {
blog: {
stale: 3600, // 1 hour
revalidate: 900, // 15 minutes
expire: 86400, // 1 day
},
},
},
}
stale
・revalidate
・expire
というキーは、それぞれ以下の役割があります。
Property | Value | Description | Requirement |
---|---|---|---|
stale | number | クライアントがサーバーをチェックせずに値をキャッシュする期間 | Optional |
revalidate | number | サーバー上でキャッシュを更新する頻度。再検証(revalidate )中に古い値が提供される場合がある。 |
Optional |
expire | number | 動的フェッチ(dynamic fetching)に切り替わる前に値が古いままでいられる最大期間。revalidate より長くする必要があります。 |
Optional |
動的フェッチ(dynamic fetching)に切り替わる前に値が古いままでいられる最大期間。
revalidate
より長くする必要があります。
上記のexpire
がドキュメントを見ても、理解が難しいところだと思うので、解説します。
そのためには、まず、revalidate
のおさらいをする必要があります。
revalidate
はキャッシュを再検証(更新)するタイミングや頻度を示すものです。
簡単に言うと、キャッシュを使い続けるために、サーバーにチェックしにいくタイミングといえます。
つまり、この設定により、定期的にキャッシュが更新され、サーバーとのデータ整合性が保たれます。
ここまでで、revalidate
について理解できたと思います。
このcacheLife
における、revalidate
を理解すると、「relivada
あるなら、expire
必要?」という疑問が発生することと思います。
結論を言うと、expire
はrevalidate
の保険のようなものです。
だからこそ、revalidate
より設定値を長くする必要があります。
expire
があることで、仮にrevalidate
が発生せず、キャッシュが更新されない場合でも、クライアントとサーバー間のデータ整合性を保つことができます。
なぜなら、expire
にはクライアントがキャッシュによる古いデータを使用し続けられる最大期間を設定するためです。
expire
の期限を過ぎたら、強制的にキャッシュは再検証されます。
これがあることで、ISRの挙動をを確実に担保することができます。
以下のように、cacheLife
の引数にnext.config
で定義したオブジェクト名を渡すだけでカスタム値を使用できます。
import { unstable_cacheLife as cacheLife } from 'next/cache'
export async function getCachedData() {
'use cache'
cacheLife('blog')
const data = await fetch('/api/data')
return data
}
以上が、大まかな機能のようです。
まとめ
これまでNext.jsはcache周辺の機能の学習コスト・導入コストが非常に高く、そこが一番のハードルと思っていたので、これはありがたいアップデートだなと個人的には思いました。
これで、日本でも、Next.jsがより広まれば尚良しという感じですね!
参考文献
Discussion