キャッシュとの歩み
投稿者: Sebastian Markbåge @ sebmarkbage
投稿日: 2024年10月24日(木)
フロントエンドのパフォーマンスを適切に実現するのは困難です。高度に最適化されたアプリでも、最も一般的な問題はクライアント・サーバー間のウォーターフォールです。Next.js App Routerを導入する際、この問題を解決したいと考えていました。そのためには、React Server Componentsを使用してクライアント・サーバー間のREST fetchを単一のラウンドトリップでサーバーに移行する必要がありました。
これは、サーバーが時として動的である必要があることを意味し、Jamstackの優れた初期読み込みパフォーマンスを犠牲にすることになりました。このトレードオフを解決し、両方の利点を得るためにpartial prerenderingを構築しました。
しかし、その過程で、提供したキャッシュのデフォルト設定とコントロールにより、開発者体験が悪化しました。fetch()のデフォルトはパフォーマンスを重視してデフォルトでキャッシュするように変更されましたが、クイックプロトタイピングや高度に動的なアプリが影響を受けました。fetch()を使用しないローカルデータベースアクセスに対する十分な制御を提供していませんでした。unstable_cache()はありましたが、使いやすくありませんでした。
これにより、export const dynamic, runtime, fetchCache, dynamicParams, revalidate = ...などのセグメントレベルの設定が回避策として必要になりました。もちろん、後方互換性のためにこれらのサポートは継続します。
しかし、少しの間、これらすべてを忘れていただきたいと思います。より簡単な何かのアイデアがあると考えています。
私たちは、<Suspense>とuse cacheという2つの概念だけに基づく新しい実験的モードを開発してきました。
冒険を選択してください
最初に気づくのは、コンポーネントにデータを追加すると、エラーが発生することです。
async function Component() {
return fetch(...)
}
export default async function Page() {
return <Component />
}
データ、Cookie、ヘッダー、現在時刻、またはランダム値を使用するには、選択肢があります:データをキャッシュしたい(サーバーまたはクライアント側)か、リクエストごとに実行したいか?
fetch()を例として使用していますが、これはデータベースやタイマーなどの任意の非同期Node APIに適用されます。
動的
まだ反復中または高度に動的なダッシュボードを構築している場合は、コンポーネントを<Suspense>境界でラップできます。<Suspense>は動的データフェッチとストリーミングを選択します。
async function Component() {
return fetch(...)
}
export default async function Page() {
return <Suspense fallback="..."><Component /></Suspense>
}
これをルートレイアウトで行うか、loading.tsxを使用することもできます。これにより、アプリのシェルが瞬時に表示されることが保証されます。Pageにさらにデータを追加し続けることができ、すべてがデフォルトで動的になることがわかります。デフォルトでは何もキャッシュされません。隠れたキャッシュはもうありません。
静的
静的なものを構築していて動的機能を使用したくない場合は、新しいuse cacheディレクティブを使用できます。
"use cache"
export default async function Page() {
return fetch(...)
}
Pageをuse cacheでマークすることで、セグメント全体がキャッシュされるべきであることを示しています。これは、フェッチするデータがキャッシュされ、ページが静的にレンダリングされることを意味します。静的コンテンツには<Suspense>境界は使用されません。ページにさらにデータを追加でき、すべてがキャッシュされます。
部分的
ミックスアンドマッチも可能です。例えば、ルートレイアウトにuse cacheを配置してキャッシュされることを保証できます。各レイアウトまたはページは独立してキャッシュできます。
"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>
)
}
特定のPage内で動的データを使用する場合:
import { Suspense } from 'react'
async function Component() {
return fetch(...)
}
export default async function Page() {
return <Suspense fallback="..."><Component /></Suspense>
}
キャッシュされた関数
このようなハイブリッドアプローチを使用する場合、API呼び出しにより近い場所でキャッシュを追加する方が便利かもしれません。use serverと同様に、任意の非同期関数にuse cacheを追加できます。サーバーを呼び出す代わりにキャッシュを呼び出すServer Actionと考えてください。
JSONを超えた豊富な型の引数と戻り値をサポートします。キャッシュキーは自動的に引数とクロージャを含むため、手動でキャッシュキーを指定する必要はありません。
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>
)
}
このレイアウトでは他のデータが使用されていないため、静的のままにできます。このアプローチの利点は、誤ってレイアウトに新しい動的データを追加した場合、ビルド中にエラーがトリガーされ、新しい選択を強制されることです。レイアウト全体にuse cacheを追加すると、エラーなしでキャッシュされます。どのアプローチを選択するかは、使用ケースによって異なります。
キャッシュのタグ付け
タグによってキャッシュエントリを明示的にクリアしたい場合は、use cache関数内で新しいcacheTag() APIを使用できます。
import { cacheTag } from 'next/cache';
async function getNotice() {
'use cache';
cacheTag('my-tag');
}
その後、以前と同様にServer ActionからrevalidateTag('my-tag')を呼び出すだけです。
このAPIはデータ読み込み後に呼び出すことができるため、データを使用してキャッシュエントリにタグを付けることができます。
import { 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;
}
キャッシュの有効期間の定義
特定のエントリまたはページがキャッシュに存在する期間を制御したい場合は、cacheLife() APIを使用できます:
"use cache"
import { cacheLife } from 'next/cache'
export default async function Page() {
cacheLife("minutes")
return ...
}
デフォルトでは、以下の値を受け入れます:
"seconds"
"minutes"
"hours"
"days"
"weeks"
"max"
使用ケースに最も適した大まかな範囲を選択してください。正確な数値を指定して、1週間が何秒(またはミリ秒?)かを計算する必要はありません。
ただし、特定の値を指定したり、独自の名前付きキャッシュプロファイルを設定することもできます。revalidateに加えて、このAPIはクライアントキャッシュのstale timeと、しばらくトラフィックがない場合にPageが期限切れになるタイミングを決定するexpireも制御できます。
実験的
これはまだ非常に実験的なプロジェクトです。まだプロダクション対応ではなく、機能不足やバグがあります。特に、この新しいタイプのエラーに対するエラースタックを改善する必要があることを認識しています。
しかし、冒険心がある場合は、早期フィードバックをお待ちしています。より詳細なアップグレードパスを公開する予定です。
早期エラーを除けば、ここでの主な破壊的変更は、fetch()のデフォルトキャッシュを元に戻すことです。とはいえ、この早期実験段階では、グリーンフィールドプロジェクトでのみ実験することをお勧めします。
うまくいけば、マイナーバージョンでオプトイン版を出荷し、将来のメジャーバージョンでデフォルトにしたいと考えています。
試すには、Next.jsのcanaryバージョンを使用する必要があります:
npx create-next-app@canary
また、next.config.tsで実験的なdynamicIOフラグを有効にする必要があります:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
dynamicIO: true,
}
};
export default nextConfig;
use cache、cacheLife、cacheTagの詳細については、ドキュメントをご覧ください。