|
| 1 | +--- |
| 2 | +source-updated-at: 2025-05-29T18:05:49.000Z |
| 3 | +translation-updated-at: 2025-06-02T19:19:48.890Z |
| 4 | +title: Next.jsにおけるコンポーザブルキャッシュ |
| 5 | +description: 'use cache' のAPI設計と利点について学びましょう |
| 6 | +author: |
| 7 | + - name: Lee Robinson |
| 8 | + image: /static/team/lee.jpg |
| 9 | +date: 2025-01-03T14:00:00.507Z |
| 10 | +image: >- |
| 11 | + https://h8DxKfmAPhn8O0p3.public.blob.vercel-storage.com/static/blog/composable-caching/twitter-card.png |
| 12 | +--- |
| 13 | + |
| 14 | +Next.js向けにシンプルで強力なキャッシュモデルを開発中です。前回の記事では、[キャッシュに関する私たちの取り組み](/blog/our-journey-with-caching)と、どのようにして`'use cache'`ディレクティブに至ったかを説明しました。 |
| 15 | + |
| 16 | +この記事では、`'use cache'`のAPI設計とその利点について議論します。 |
| 17 | + |
| 18 | +[`'use cache'`とは?](#what-is-use-cache) |
| 19 | +-------------------------------------------- |
| 20 | + |
| 21 | +`'use cache'`は、必要に応じてデータやコンポーネントをキャッシュすることでアプリケーションを高速化します。 |
| 22 | + |
| 23 | +これはJavaScriptの「ディレクティブ」—コードに追加する文字列リテラル—であり、Next.jsコンパイラに異なる「境界」に入るよう信号を送ります。例えば、サーバーからクライアントへの移行などです。 |
| 24 | + |
| 25 | +これはReactの`'use client'`や`'use server'`のようなディレクティブと同様の考え方です。ディレクティブはコードがどこで実行されるべきかを定義するコンパイラ指令で、フレームワークが個々の部品を最適化し調整できるようにします。 |
| 26 | + |
| 27 | +[どのように機能するか?](#how-does-it-work) |
| 28 | +-------------------------------------- |
| 29 | + |
| 30 | +簡単な例から始めましょう: |
| 31 | + |
| 32 | +``` |
| 33 | +async function getUser(id) { |
| 34 | + 'use cache'; |
| 35 | + let res = await fetch(`https://api.vercel.app/user/${id}`); |
| 36 | + return res.json(); |
| 37 | +} |
| 38 | +``` |
| 39 | + |
| 40 | +内部では、Next.jsは`'use cache'`ディレクティブによりこのコードをサーバー関数に変換します。コンパイル時に、このキャッシュエントリの「依存関係」が見つかり、キャッシュキーの一部として使用されます。 |
| 41 | + |
| 42 | +例えば、`id`はキャッシュキーの一部になります。`getUser(1)`を複数回呼び出すと、キャッシュされたサーバー関数からメモ化された出力が返されます。この値を変更すると、キャッシュに新しいエントリが作成されます。 |
| 43 | + |
| 44 | +[クロージャ](https://v0.dev/chat/5kD47RIecQK?b=b_rCP4CvfbFFW)を使用したサーバーコンポーネントでのキャッシュ関数の使用例を見てみましょう。 |
| 45 | + |
| 46 | +``` |
| 47 | +function Profile({ id }) { |
| 48 | + async function getNotifications(index, limit) { |
| 49 | + 'use cache'; |
| 50 | + return await db |
| 51 | + .select() |
| 52 | + .from(notifications) |
| 53 | + .limit(limit) |
| 54 | + .offset(index) |
| 55 | + .where(eq(notifications.userId, id)); |
| 56 | + } |
| 57 | + |
| 58 | + return <User notifications={getNotifications} />; |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +この例はより複雑です。キャッシュキーの一部となるべきすべての依存関係を見つけられますか? |
| 63 | + |
| 64 | +引数`index`と`limit`は理解しやすい—これらの値が変わると、通知の異なるスライスを選択します。しかし、ユーザー`id`についてはどうでしょうか?その値は親コンポーネントから来ています。 |
| 65 | + |
| 66 | +コンパイラは`getNotifications`が`id`にも依存していることを理解でき、その値は自動的にキャッシュキーに含まれます。これにより、キャッシュキーにおける不正確または欠落した依存関係によるキャッシュ問題の全カテゴリが防止されます。 |
| 67 | + |
| 68 | +[キャッシュ関数を使用しない理由](#why-not-use-a-cache-function) |
| 69 | +-------------------------------------------------------------- |
| 70 | + |
| 71 | +前の例を再考しましょう。ディレクティブの代わりに`cache()`関数を使用することはできませんか? |
| 72 | + |
| 73 | +``` |
| 74 | +function Profile({ id }) { |
| 75 | + async function getNotifications(index, limit) { |
| 76 | + return await cache(async () => { |
| 77 | + return await db |
| 78 | + .select() |
| 79 | + .from(notifications) |
| 80 | + .limit(limit) |
| 81 | + .offset(index) |
| 82 | + // おっと!idをキャッシュキーに含める場所は? |
| 83 | + .where(eq(notifications.userId, id)); |
| 84 | + }); |
| 85 | + } |
| 86 | + |
| 87 | + return <User notifications={getNotifications} />; |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +`cache()`関数はクロージャを調べて`id`値をキャッシュキーの一部に含めるべきか判断できません。手動で`id`がキーの一部であることを指定する必要があります。これを忘れたり、間違えたりすると、キャッシュ衝突や古いデータのリスクがあります。 |
| 92 | + |
| 93 | +クロージャはあらゆる種類のローカル変数を捕捉できます。単純なアプローチでは、意図しない変数を誤って組み込んだり(または省略したり)する可能性があります。これにより、間違ったデータがキャッシュされたり、機密情報がキャッシュキーに漏れることでキャッシュ汚染のリスクが生じたりする可能性があります。 |
| 94 | + |
| 95 | +`'use cache'`はコンパイラにクロージャを安全に処理し、キャッシュキーを正しく生成するための十分なコンテキストを提供します。`cache()`のようなランタイムのみのソリューションでは、すべてを手動で行う必要があり、ミスを犯しやすくなります。対照的に、ディレクティブは静的に分析でき、すべての依存関係を確実に処理できます。 |
| 96 | + |
| 97 | +[シリアライズ不可能な入力値はどのように処理されるか?](#how-are-non-serialized-input-values-handled) |
| 98 | +-------------------------------------------------------------------------------------------- |
| 99 | + |
| 100 | +キャッシュする入力値には2つの異なるタイプがあります: |
| 101 | + |
| 102 | +* **シリアライズ可能**: ここで「シリアライズ可能」とは、意味を失うことなく安定した文字列ベースの形式に変換できる入力を指します。多くの人がまず`JSON.stringify`を考えますが、実際にはReactのシリアライゼーション(例えばサーバーコンポーネント経由)を使用して、Promise、循環データ構造、その他の複雑なオブジェクトなど、より広範な入力を処理します。これはプレーンなJSONができることを超えています。 |
| 103 | +* **シリアライズ不可能**: これらの入力はキャッシュキーの一部ではありません。これらの値をキャッシュしようとすると、サーバー「参照」を返します。この参照はNext.jsによってランタイム時に元の値に復元されます。 |
| 104 | + |
| 105 | +`id`をキャッシュキーに含めることを覚えていたとしましょう: |
| 106 | + |
| 107 | +``` |
| 108 | +await cache(async () => { |
| 109 | + return await db |
| 110 | + .select() |
| 111 | + .from(notifications) |
| 112 | + .limit(limit) |
| 113 | + .offset(index) |
| 114 | + .where(eq(notifications.userId, id)); |
| 115 | +}, [id, index, limit]); |
| 116 | +``` |
| 117 | + |
| 118 | +これは入力値がシリアライズ可能な場合に機能します。しかし、`id`がReact要素やより複雑な値だった場合、入力キーを手動でシリアライズする必要があります。`id`プロパティに基づいて現在のユーザーを取得するサーバーコンポーネントを考えてみましょう: |
| 119 | + |
| 120 | +``` |
| 121 | +async function Profile({ id, children }) { |
| 122 | + 'use cache'; |
| 123 | + const user = await getUser(id); |
| 124 | + |
| 125 | + return ( |
| 126 | + <> |
| 127 | + <h1>{user.name}</h1> |
| 128 | + {/* childrenを変更してもキャッシュが破綻しない...なぜ? */} |
| 129 | + {children} |
| 130 | + </> |
| 131 | + ); |
| 132 | +} |
| 133 | +``` |
| 134 | + |
| 135 | +これがどのように機能するか見ていきましょう: |
| 136 | + |
| 137 | +1. コンパイル時に、Next.jsは`'use cache'`ディレクティブを認識し、キャッシュをサポートする特別なサーバー関数を作成するようにコードを変換します。コンパイル時にキャッシュが行われるわけではなく、Next.jsはランタイムキャッシュに必要なメカニズムを設定しています。 |
| 138 | +2. コードが「キャッシュ関数」を呼び出すと、Next.jsは関数の引数をシリアライズします。JSXのように直接シリアライズできないものは「参照」プレースホルダーに置き換えられます。 |
| 139 | +3. Next.jsは、指定されたシリアライズされた引数に対してキャッシュされた結果が存在するかどうかを確認します。結果が見つからない場合、関数はキャッシュする新しい値を計算します。 |
| 140 | +4. 関数が終了すると、戻り値がシリアライズされます。戻り値のシリアライズ不可能な部分は参照に戻されます。 |
| 141 | +5. キャッシュ関数を呼び出したコードは出力をデシリアライズし、参照を評価します。これにより、Next.jsは参照を実際のオブジェクトや値と交換できるため、`children`のようなシリアライズ不可能な入力も元の、キャッシュされていない値を保持できます。 |
| 142 | + |
| 143 | +これは、`<Profile>`コンポーネントだけを安全にキャッシュし、子をキャッシュしないことを意味します。後続のレンダリングでは、`getUser()`は再度呼び出されません。`children`の値は動的であるか、異なるキャッシュ寿命を持つ別個のキャッシュ要素かもしれません。これがコンポーザブルキャッシュです。 |
| 144 | + |
| 145 | +[これは見覚えが...](#this-seems-familiar) |
| 146 | +-------------------------------------------- |
| 147 | + |
| 148 | +「サーバーとクライアントのコンポジションと同じモデルのように感じる」と考えているなら、まったく正しいです。これは時々「ドーナツ」パターンと呼ばれます: |
| 149 | + |
| 150 | +* **外側**の部分はデータ取得や重いロジックを処理するサーバーコンポーネント |
| 151 | +* 中央の**穴**はインタラクティブ性を持つかもしれない子コンポーネント |
| 152 | + |
| 153 | +```tsx filename="app/page.tsx" |
| 154 | +export default function Page() { |
| 155 | + return ( |
| 156 | + <ServerComponent> |
| 157 | + {/* クライアントへの穴を作成 */} |
| 158 | + <ClientComponent /> |
| 159 | + <ServerComponent /> |
| 160 | + ); |
| 161 | +} |
| 162 | +``` |
| 163 | + |
| 164 | +`'use cache'`も同じです。ドーナツは外側のコンポーネントのキャッシュされた値で、穴はランタイム時に埋められる参照です。これが`children`を変更してもキャッシュされた出力全体が無効化されない理由です。子は後で埋められる参照に過ぎません。 |
| 165 | + |
| 166 | +[タグ付けと無効化については?](#what-about-tagging-and-invalidation) |
| 167 | +---------------------------------------------------------------------------- |
| 168 | + |
| 169 | +キャッシュの寿命はさまざまな[プロファイル](/docs/app/api-reference/functions/cacheLife)で定義できます。デフォルトのプロファイルセットが含まれていますが、必要に応じてカスタム値を定義できます。 |
| 170 | + |
| 171 | +``` |
| 172 | +async function getUser(id) { |
| 173 | + 'use cache'; |
| 174 | + cacheLife('hours'); |
| 175 | + let res = await fetch(`https://api.vercel.app/user/${id}`); |
| 176 | + return res.json(); |
| 177 | +} |
| 178 | +``` |
| 179 | + |
| 180 | +特定のキャッシュエントリを無効化するには、[キャッシュにタグを付け](/docs/app/api-reference/functions/cacheTag)、`revalidateTag()`を呼び出します。強力なパターンの1つは、データ(例えばCMSから)を取得した後にキャッシュにタグを付けられることです: |
| 181 | + |
| 182 | +``` |
| 183 | +async function getPost(postId) { |
| 184 | + 'use cache'; |
| 185 | + let res = await fetch(`https://api.vercel.app/blog/${postId}`); |
| 186 | + let data = await res.json(); |
| 187 | + cacheTag(postId, data.authorId); |
| 188 | + return data; |
| 189 | +} |
| 190 | +``` |
| 191 | + |
| 192 | +[シンプルで強力](#simple-and-powerful) |
| 193 | +------------------------------------------- |
| 194 | + |
| 195 | +`'use cache'`の目標は、キャッシュロジックの作成をシンプルかつ強力にすることです。 |
| 196 | + |
| 197 | +* **シンプル**: ローカルな推論でキャッシュエントリを作成できます。キャッシュキーエントリの忘れや、コードベースの他の部分への意図しない変更といったグローバルな副作用を心配する必要はありません。 |
| 198 | +* **強力**: 静的に分析可能なコードだけでなく、ランタイム時に変更される可能性のある値など、より多くのものをキャッシュできます。 |
| 199 | + |
| 200 | +`'use cache`はNext.js内部ではまだ**実験的**です。テストする際の早期フィードバックをお待ちしています。 |
| 201 | + |
| 202 | +[ドキュメントで詳細を確認](/docs/app/api-reference/directives/use-cache)。 |
0 commit comments