なぜAPI戦略にキャッシュが必要なのか
1つのAPIリクエストが、一連の重い処理を引き起こすことは珍しくありません。サーバーはデータベースへのクエリ、ビジネスロジックの処理、JSONのシリアライズに200msを費やすかもしれません。もし前回のキャッシュからデータが変わっていなければ、その処理は無駄になります。ヘッダーを少し調整するだけで、不要なデータ転送を防ぎ、クラウドのデータ転送コストを月間数千ドル削減できた事例を私は見てきました。
キャッシュの本質は、不必要な作業を避けることにあります。リソースの保存されたコピーを返すことで、読み取り主体のアプリケーションにおけるサーバー負荷を大幅に軽減できます。これは単なるパフォーマンス向上ではありません。予期せぬトラフィックの急増時にインフラを維持するための、不可欠なセーフティネットにもなります。
コアとなる2つの柱:鮮度(Freshness)と検証(Validation)
キャッシュを始めるのにRedisや複雑なインフラは必要ありません。HTTPプロトコルには、これらの機能が組み込まれています。正しく実装するには、「鮮度(Freshness)」と「検証(Validation)」という2つの概念をマスターするだけで十分です。
鮮度ヘッダーは、リソースがいつまで有効であるかをクライアントに伝えます。Cache-Controlヘッダーは「有効期限」のようなものだと考えてください。一方、ETagのような検証ヘッダーは「チェックサム」として機能します。これにより、クライアントは「このデータのバージョンXを持っていますが、本当に再ダウンロードする必要がありますか?」とサーバーに問い合わせることができます。
In modern environments like Node.jsやGoのような現代的な環境では、キャッシュを「インストール」することは稀です。代わりに、ミドルウェアを設定してレスポンスにこれらのヘッダーを挿入します。以下は、最適化を行う前の標準的なExpressのエンドポイントです。
const express = require('express');
const app = express();
app.get('/api/products', (req, res) => {
// 商品データの配列を作成
const products = [{ id: 1, name: 'ノートPC' }, { id: 2, name: 'スマートフォン' }];
res.json(products);
});
app.listen(3000, () => console.log('サーバーがポート3000で起動しました'));
適切なCache-Controlポリシーの選択
Cache-Controlヘッダーは、パフォーマンスを制御するための主要なレバーです。これはブラウザやCDNに対する複数の指示(ディレクティブ)を組み合わせたものです。設定ミスは「古いデータ(Stale Data)」が表示されるバグの一般的な原因となるため、正確さが求められます。
Public vs. Private
50件の公開ブログ記事のような一般的なデータは、publicとしてマークする必要があります。これにより、CDNがレスポンスを保存し、数千人のユーザーに配信できるようになります。逆に、請求ダッシュボードのようなユーザー固有のデータはprivateにする必要があります。これにより、特定のユーザーのブラウザのみがデータを保存し、機密情報が共有の中間サーバーに残らないようにします。
max-ageの設定
max-ageディレクティブは、有効期間(TTL)を秒単位で定義します。1時間に1回更新される製品カタログの場合は3600を使用します。決して変更されない静的アセットの場合は、1年(31536000)という長い値を設定することもあります。
Cache-Control: public, max-age=3600
No-CacheとNo-Storeの違い
- no-cache: 少し紛らわしい名称ですが、これはブラウザに対して「データを保存してもよいが、使用する前に必ずサーバーに確認しなければならない」と伝えます。古い情報を表示するリスクを避けつつ、ETagのスピードを享受したい場合に最適です。
- no-store: パスワードリセットトークンのようなセキュリティ性の高いエンドポイントで使用します。ブラウザやすべてのプロキシに対して、データのコピーを一切保存することを禁止します。
ETagによる帯域幅の節約
ETag(Entity Tag)は、検証側の仕組みを担います。ETagは、リソースの現在の状態を表す一意の文字列(多くの場合MD5ハッシュ)です。データが変更されると、ハッシュも変更されます。
ワークフローは単純です。まず、サーバーがETag: "v123"を含むレスポンスを送信します。次回のクエストで、クライアントはその値をIf-None-Match: "v123"ヘッダーに入れて返します。データが同一であれば、サーバーは**304 Not Modified**ステータスを返します。レスポンスボディは空になるため、大きなJSONペイロードにおいて大幅な帯域幅の節約になります。
const crypto = require('crypto');
app.get('/api/data', (req, res) => {
const data = { message: "こんにちは、世界", timestamp: "2023-10-27" };
const jsonString = JSON.stringify(data);
// データのMD5ハッシュを生成してETagとする
const etag = crypto.createHash('md5').update(jsonString).digest('hex');
// クライアントが持っているハッシュと一致するか確認
if (req.headers['if-none-match'] === etag) {
return res.status(304).end();
}
res.setHeader('ETag', etag);
res.setHeader('Cache-Control', 'public, no-cache');
res.send(data);
});
フレームワークがこれを自動化していることも多いですが、手動で実装してみることでバグに気づきやすくなります。例えば、JSONに “generated_at”(生成日時)タイムスタンプが含まれていると、リクエストのたびにETagが変わってしまい、キャッシュが役に立たなくなります。
実装の検証
コードを書いただけでキャッシュが動作していると思い込まないでください。ブラウザの**ネットワーク**タブを開き、「サイズ」列を確認しましょう。「(from disk cache)」と表示されているか、ステータスが「304」になっていれば成功です。
cURLでのテスト
ヘッダーを素早く確認するにはcurlが便利です。-Iフラグを使用すると、ボディを表示せずにヘッダーのみを確認できます。
curl -I http://localhost:3000/api/data
再訪問したユーザーをシミュレートするには、サーバーにETagを返します。
curl -I -H 'If-None-Match: "ここにハッシュを入力"' http://localhost:3000/api/data
設定が正しければ、HTTP/1.1 304 Not Modifiedが返されます。
Varyヘッダーに注意
よくある落とし穴は、Varyヘッダーを無視することです。APIがAuthorizationやAccept-Languageヘッダーに基づいて異なるコンテンツを返す場合、それをキャッシュに伝える必要があります。そうしないと、ユーザーAが誤ってユーザーBのキャッシュデータを見てしまう可能性があります。
Vary: Authorization, Accept-Encoding
これらのヘッダーを使いこなすことで、脆弱なAPIを堅牢でスケーラブルなシステムに変えることができます。最小限のコードで大きな効果が得られる最適化であり、ユーザーベースの拡大に合わせて大きなパフォーマンス向上をもたらします。

