2024年10月24日木曜日
キャッシュとの旅
投稿者フロントエンドのパフォーマンスを適切に実現するのは難しい場合があります。高度に最適化されたアプリでも、最も一般的な原因はクライアント・サーバーのウォーターフォールです。Next.js App Routerを導入するにあたり、この問題を解決したいと考えていました。そのために、クライアント・サーバー間のRESTフェッチを、React Server Componentsを使用して単一のラウンドトリップでサーバーに移動させる必要がありました。これにより、サーバーは動的になる必要があり、Jamstackの優れた初期ロードパフォーマンスを犠牲にすることもありました。私たちはPartial Prerenderingを構築して、このトレードオフを解決し、両方の長所を活かせるようにしました。
しかし、その過程で、提供したキャッシュのデフォルト設定や制御機能により、開発者体験が損なわれました。fetch()のデフォルトは、デフォルトでキャッシュを優先するように変更されましたが、迅速なプロトタイピングや高度に動的なアプリは苦戦しました。fetch()を使用しないローカルデータベースへのアクセスを十分に制御できませんでした。unstable_cache()はありましたが、使いやすさに欠けていました。これにより、export const dynamic, runtime, fetchCache, dynamicParams, revalidate = ...のようなセグメントレベルの設定が、エスケープハッチとして必要になりました。
もちろん、後方互換性のために引き続きサポートします。しかし、ここでは一度、それらのことはすべて忘れてください。私たちは、よりシンプルなアイデアを思いつきました。
私たちは、<Suspense>とuse cacheという2つの概念に基づいた、新しい実験的なモードを開発しています。
冒険を選んでください
まず気づくのは、コンポーネントにデータを追加すると、エラーが発生するようになったことです。
async function Component() {
return fetch(...) // error
}
export default async function Page() {
return <Component />
}データ、クッキー、ヘッダー、現在時刻、または乱数を使用するには、選択肢ができました。データをキャッシュ(サーバーまたはクライアントサイド)するか、リクエストごとに実行するかです。例としてfetch()を使用していますが、これはデータベースやタイマーなどの非同期Node APIにも同様に適用されます。
動的
まだイテレーション中であったり、高度に動的なダッシュボードを構築している場合は、コンポーネントを<Suspense>境界でラップできます。<Suspense>は、動的なデータフェッチとストリーミングをオプトインします。
async function Component() {
return fetch(...) // no error
}
export default async function Page() {
return <Suspense fallback="..."><Component /></Suspense>
}ルートレイアウトで行うか、loading.tsxを使用することもできます。
これにより、アプリのシェルは瞬時に表示され続けます。ページ内にさらにデータを追加しても、すべてデフォルトで動的になります。デフォルトでは何もキャッシュされません。隠されたキャッシュはもうありません。
静的
静的なものを構築していて動的な機能を使用したくない場合は、新しいuse cacheディレクティブを使用できます。
"use cache"
export default async function Page() {
return fetch(...) // no error
}ページを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>
}特定のページ内で動的なデータを使用する場合
import { Suspense } from 'react'
async function Component() {
return fetch(...) // no error
}
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 ...
}デフォルトでは、次の値を受け入れます。
秒分時間日週最大
ユースケースに最も適した大まかな範囲を選択してください。秒(または週あたりのミリ秒数?)を正確に計算して指定する必要はありません。ただし、特定の値を指定したり、独自の名前付きキャッシュプロファイルを構成することもできます。
revalidateに加えて、このAPIはクライアントキャッシュのstale時間と、しばらくトラフィックがない場合にページが期限切れになるタイミングを決定するexpireを制御できます。
実験的
これはまだ非常に実験的なプロジェクトです。まだ本番環境で利用できる状態ではなく、欠けている機能やバグも存在します。特に、この新しい種類のエラーのエラースタックを改善する必要があることを認識しています。しかし、もしあなたが冒険心があるなら、早期のフィードバックをお待ちしています。
より詳細なアップグレードパスを公開します。早期のエラーを除けば、主な破壊的変更はfetch()のデフォルトキャッシュを元に戻すことです。とはいえ、この初期の実験段階では、グリーンフィールドプロジェクトでのみ実験することをお勧めします。うまくいけば、マイナーアップデートでオプトインバージョンをリリースし、将来のメジャーアップデートでデフォルトにする予定です。
これを試すには、Next.jsのcanaryバージョンを使用する必要があります。
npx create-next-app@canarynext.config.tsで実験的なdynamicIOフラグを有効にする必要もあります。
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
dynamicIO: true,
}
};
export default nextConfig;