はじめに
Webアプリケーションのパフォーマンス向上において、キャッシュ戦略の最適化は避けて通れません。適切なキャッシュを活用することで、ページロードの高速化・サーバー負荷の軽減・ユーザーエクスペリエンスの向上を実現できます。
本記事では、CDN・Redis / Memcached・APIレスポンスキャッシュ・Next.js ISR / SSG・クライアントサイドキャッシュなど、さまざまなキャッシュ技術を駆使して、パフォーマンスを最大化する方法を解説します。キャッシュを適切に管理することで、よりスケーラブルで効率的なシステムを構築することが可能になります。
それでは、具体的な戦略を見ていきましょう。
CDN(Cloudflare / AWS CloudFront)による静的コンテンツの最適配信
CDN(Content Delivery Network)は、静的コンテンツ(HTML、CSS、JavaScript、画像など)を世界中のエッジサーバーに分散配置し、ユーザーの最寄りのエッジサーバーから配信することで、ロード時間を短縮し、オリジンサーバーの負荷を軽減します。
CDNの仕組み
CDNは、以下のような流れで動作します。
- ユーザーがWebサイトを開くと、ブラウザはCDNのエッジサーバーにリクエストを送る。
- エッジサーバーにキャッシュがある場合、そのままキャッシュされたコンテンツを提供(キャッシュヒット)。
- キャッシュがない場合(キャッシュミス)、オリジンサーバーからコンテンツを取得し、キャッシュに保存した後にユーザーへ提供。
- 次回以降、他のユーザーもエッジサーバーからキャッシュ済みコンテンツを取得可能。
主な利点
- 遅延の低減
- 地理的に分散したエッジサーバーにより、物理的に近いサーバーから配信されるため、レイテンシが低くなる。
- サーバー負荷の軽減
- オリジンサーバーへのリクエストが削減されるため、バックエンドの負荷が軽減し、スケールしやすくなる。
- セキュリティ向上
- DDoS攻撃対策、WAF(Web Application Firewall)、Bot対策などのセキュリティ機能を提供。
Cloudflare と AWS CloudFront の比較
項目 | Cloudflare | AWS CloudFront |
---|---|---|
キャッシュ制御 | 柔軟なルール設定が可能(Cache Rules, Page Rules) | S3 や Lambda@Edge との統合が容易 |
DDoS対策 | 標準機能として無料で提供 | AWS Shield(有料)と統合 |
TLS/SSL | 自動で無料のSSLを提供 | ACM(AWS Certificate Manager)で管理 |
価格 | 無料プランあり | 使用量に応じた課金 |
Cloudflare を使った静的サイトの設定
- ドメインをCloudflareに登録
Cloudflareの管理画面からドメインを追加し、DNS設定を変更。 - キャッシュ設定
Cache Rulesを使用して、キャッシュポリシーを設定。
# Cache-Controlヘッダーの設定(静的ファイルのキャッシュ)
location ~* \.(css|js|jpg|png|gif|ico|svg|woff|woff2|ttf|otf)$ {
expires 30d;
add_header Cache-Control "public, max-age=2592000";
}
AWS CloudFront を使ったS3連携
例)CloudFrontをS3のオリジンとして設定
- S3バケットを作成し、静的ウェブサイトとして設定
- CloudFrontのオリジンにS3を指定
- キャッシュ動作のカスタマイズ
Cache Policyを作成し、特定のリクエストヘッダーをキャッシュキーに含める。
{
"name": "MyCustomPolicy",
"minTTL": 60,
"maxTTL": 86400,
"defaultTTL": 3600,
"parametersInCacheKeyAndForwardedToOrigin": {
"headersConfig": {
"headerBehavior": "whitelist",
"headers": ["Authorization"]
}
}
}
Redis / Memcached によるデータベース負荷軽減
データベースへの負荷を軽減するために、頻繁にアクセスされるデータをメモリ上にキャッシュするのが Redis や Memcached の基本的な役割です。
例えば、データベースに対する 同じクエリが頻繁に実行 される場合、結果をキャッシュに保存することで、
- データベースの負荷を大幅に削減
- クエリ応答速度の向上 が可能になります。
Redis と Memcached の違い
特徴 | Redis | Memcached |
---|---|---|
データ構造 | リスト、セット、ハッシュなど多彩 | シンプルなキー・バリュー型 |
永続性 | オプションでディスク保存可 | メモリのみ(永続性なし) |
スケーラビリティ | マスター・スレーブ構成 | シンプルな分散構成 |
トランザクション | サポート(MULTI/EXEC) | 非対応 |
メモリ管理 | LRU(Least Recently Used)など選択可能 | 自動削除(LRUのみ) |
用途 | セッション管理、ランキング、キュー | シンプルなデータキャッシュ |
Redis
: 多機能で永続化も可能なため、幅広い用途に適用できるMemcached
: 単純なキー・バリューキャッシュ用途に特化し、高速
具体的な適用例
頻繁にクエリされるデータ(ランキング・設定値)をキャッシュ
頻繁にアクセスされるランキングデータをキャッシュすることで、DB 負荷を軽減できます。
const { Pool } = require('pg');
const redis = require('redis');
const { promisify } = require('util');
const db = new Pool({ connectionString: 'postgres://user:password@localhost:5432/mydb' });
const redisClient = redis.createClient();
const getAsync = promisify(redisClient.get).bind(redisClient);
const setAsync = promisify(redisClient.set).bind(redisClient);
async function getLeaderboard(req, res) {
const cacheKey = 'leaderboard';
// Redis にキャッシュされているか確認
const cachedData = await getAsync(cacheKey);
if (cachedData) {
return res.json(JSON.parse(cachedData));
}
// データベースからランキングデータを取得
const result = await db.query('SELECT * FROM leaderboard ORDER BY score DESC LIMIT 10');
// Redis にデータをキャッシュ(有効期限 10 分)
await setAsync(cacheKey, JSON.stringify(result.rows), 'EX', 600);
res.json(result.rows);
}
app.get('/leaderboard', getLeaderboard);
ポイント
leaderboard
というキーでランキングデータをRedis
にキャッシュ- DB へのクエリを減らし、負荷を軽減
- キャッシュの有効期限(10 分)を設定して、古くなったデータを自動削除
計算コストの高いデータの一時保存
例えば、データ分析の結果や一時的な集計結果をキャッシュすることで、同じ計算を繰り返すのを防げます。
from flask import Flask, jsonify
import memcache
app = Flask(__name__)
cache = memcache.Client(['127.0.0.1:11211'])
def expensive_computation():
return sum(range(1000000)) # 計算コストの高い処理
@app.route('/compute')
def compute():
cache_key = "expensive_result"
# キャッシュから取得
cached_result = cache.get(cache_key)
if cached_result:
return jsonify({"result": cached_result, "cached": True})
# 計算処理
result = expensive_computation()
# キャッシュに保存(有効期限 30 秒)
cache.set(cache_key, result, time=30)
return jsonify({"result": result, "cached": False})
if __name__ == '__main__':
app.run(debug=True)
ポイント
- 計算結果を
expensive_result
というキーで Memcached に保存 - キャッシュがあれば、それを返して計算を省略
- 30 秒でキャッシュが無効化され、最新の計算結果を取得可能
API のレスポンスキャッシュの適用(GraphQL / REST API の最適化)
APIのレスポンスキャッシュを適用することで、オリジンサーバーの負荷を軽減し、クライアントのレスポンス速度を向上させることができます。GraphQLとREST APIそれぞれで有効なキャッシュ戦略を解説し、サンプルコードも交えて説明します。
Apollo Client のキャッシュ機能
フロントエンドでApollo Clientを利用する場合、デフォルトで InMemoryCache を使用してクエリ結果をキャッシュします。これにより、同じデータへのリクエストが発生した際に、オリジンサーバーではなくローカルキャッシュからデータを取得できます。
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
// Apollo Client の設定
const client = new ApolloClient({
uri: 'https://example.com/graphql',
cache: new InMemoryCache(),
});
// キャッシュを有効活用したクエリ
client.query({
query: gql`
query GetUser {
user(id: "1") {
id
name
}
}
`,
}).then(response => console.log(response.data));
ポイント
- InMemoryCache により、クエリ結果がキャッシュされ、同じデータをリクエストするとキャッシュが活用される。
- cache-first ポリシーがデフォルトで適用され、キャッシュにデータがある場合はネットワークリクエストを行わない。
CDN を活用した API レスポンスキャッシュ
エッジサーバー(Cloudflare、Fastly、AWS CloudFront など)でキャッシュを行うことで、クライアントからのリクエストをオリジンサーバーに転送する回数を減らせます。
下記はGraphQL API でキャッシュを適用する CDN 設定例(Cloudflare Workers)
addEventListener("fetch", event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const cacheUrl = new URL(request.url);
const cache = caches.default;
let response = await cache.match(cacheUrl);
if (!response) {
response = await fetch(request);
// キャッシュを設定(60秒間有効)
response = new Response(response.body, response);
response.headers.append("Cache-Control", "s-maxage=60");
await cache.put(cacheUrl, response.clone());
}
return response;
}
s-maxage=60
を設定し、CDNで60秒間キャッシュする。Cloudflare Workers
でキャッシュを管理することで、オリジンサーバーの負荷を大幅に削減可能。
Redis でデータキャッシュ
Redisを使用して、リゾルバ単位でキャッシュを適用できます。特に、頻繁にアクセスされるデータ(例:ランキング、ニュースフィード)に有効です。
例)Apollo Server で Redis キャッシュを実装
import { ApolloServer, gql } from 'apollo-server-express';
import Redis from 'ioredis';
const redis = new Redis();
const typeDefs = gql`
type User {
id: ID!
name: String!
}
type Query {
user(id: ID!): User
}
`;
const resolvers = {
Query: {
user: async (_, { id }) => {
const cacheKey = `user:${id}`;
const cachedData = await redis.get(cacheKey);
if (cachedData) {
return JSON.parse(cachedData);
}
const user = { id, name: "John Doe" }; // DB から取得するデータ(例)
await redis.set(cacheKey, JSON.stringify(user), "EX", 60); // 60秒キャッシュ
return user;
},
},
};
const server = new ApolloServer({ typeDefs, resolvers });
ポイント
redis.get(cacheKey)
でキャッシュチェックし、ヒットした場合はDBアクセスを回避。redis.set(cacheKey, JSON.stringify(user), "EX", 60)
で60秒間キャッシュを保持。
ETag / Last-Modified ヘッダーを活用
REST APIでETag や Last-Modified を利用することで、リクエストごとにデータの変更をチェックし、変更がない場合はキャッシュを再利用できます。
import express from "express";
import crypto from "crypto";
const app = express();
app.get("/data", (req, res) => {
const data = { message: "Hello, World!" };
const etag = crypto.createHash("md5").update(JSON.stringify(data)).digest("hex");
res.setHeader("ETag", etag);
if (req.headers["if-none-match"] === etag) {
res.status(304).end(); // 304: Not Modified
} else {
res.json(data);
}
});
app.listen(3000, () => console.log("Server running on port 3000"));
ポイント
- クライアントは
ETag
をIf-None-Match
ヘッダーとして送信し、変更がなければ304 Not Modified
を返す。 - 不必要なデータ転送を削減。
Cache-Control: max-age 指定によるブラウザキャッシュ
REST APIでブラウザに API レスポンスをキャッシュさせることで、不要なリクエストを防ぎます。
app.get("/data", (req, res) => {
res.setHeader("Cache-Control", "public, max-age=3600"); // 1時間キャッシュ
res.json({ message: "Hello, Cached World!" });
});
ポイント
max-age=3600
で、1時間はブラウザキャッシュを利用。
ユーザーセッションのキャッシュ戦略の改善
セッション情報を効率的にキャッシュすることで、認証処理の負荷を軽減し、アプリケーションのパフォーマンスを向上させることができます。ここでは、一般的な セッションキャッシュの方法 について詳しく解説し、それぞれの方法のサンプルコードを紹介します。
Redis を使用したセッション管理(サーバーサイドセッション)
概要
- セッション情報を Redis に保存し、ユーザー認証時のデータ取得を高速化する。
- ユーザーごとに一意のセッション ID を発行し、Redis に保存。
- スケーラブルなセッション管理が可能。
メリット
- ユーザーごとの状態を保持できる(Stateful)。
- サーバーがセッション情報を管理するため、トークンの解析負荷がない。
- 期限切れのセッションを Redis 側で自動管理できる。
デメリット
- 負荷分散を考慮した場合、セッション情報を共有する仕組み(Redis クラスター等)が必要。
- JWT と比較してスケーラビリティが低い。
下記のコードは Express.js を使用して Redis にセッションを保存するサーバー を構築するものです。
import express from 'express';
import session from 'express-session';
import Redis from 'ioredis';
import RedisStore from 'connect-redis';
const app = express();
const redisClient = new Redis();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: 'your_secret_key', // 環境変数で管理
resave: false,
saveUninitialized: false,
cookie: { secure: false, httpOnly: true, maxAge: 1000 * 60 * 60 } // 1時間
}));
app.get('/login', (req, res) => {
req.session.user = { id: 1, name: "John Doe" };
res.send("Logged in");
});
app.get('/profile', (req, res) => {
if (!req.session.user) return res.status(401).send("Unauthorized");
res.json(req.session.user);
});
app.listen(3000, () => console.log("Server running on port 3000"));
express-session
と connect-redis
を組み合わせることで、セッション情報を Redis に保存し、サーバーが再起動してもセッションを維持 できるようにしています。
このコードの動作は以下のようになります。
- ユーザーが
/login
にアクセスすると、セッション情報が Redis に保存される。 - ユーザーが
/profile
にアクセスすると、セッション情報がある場合に認証されたユーザー情報を返す。 - Redis に保存されたセッション情報を connect-redis を使って管理し、メモリ上ではなく Redis を利用してスケールしやすくする。
セッション情報は Redis に以下のような形式で保存されます。
{
"sess:U2FtMWJhMWVh...": {
"cookie": {
"originalMaxAge": 3600000,
"expires": "2025-03-07T12:00:00.000Z",
"httpOnly": true,
"path": "/"
},
"user": {
"id": 1,
"name": "John Doe"
}
}
}
JWT(JSON Web Token)によるステートレス認証
概要
- サーバー側でセッションを保持せず、トークンをクライアントに渡し、リクエストごとに認証する。
- トークンには署名が含まれ、不正改ざんを防げる。
メリット
- スケーラビリティが高い(サーバーレスやマルチインスタンスに適している)。
- Redis などの外部ストレージが不要。
- 認証情報を持ち回るため、レスポンス速度が速い。
デメリット
- トークンの無効化が難しい(トークンが漏れた場合、期限が切れるまで有効)。
- 署名の検証コストが発生する(毎回 JWT の解析が必要)。
下記のコードは Express + JSON Web Token(JWT)を用いたステートレス認証 を実装しています。
import express from 'express';
import jwt from 'jsonwebtoken';
const app = express();
const SECRET_KEY = "your_secret_key"; // 環境変数で管理
app.use(express.json());
app.post('/login', (req, res) => {
const user = { id: 1, name: "John Doe" };
const token = jwt.sign(user, SECRET_KEY, { expiresIn: '1h' });
res.json({ token });
});
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(401).send("Unauthorized");
try {
req.user = jwt.verify(token, SECRET_KEY);
next();
} catch (err) {
res.status(403).send("Invalid token");
}
};
app.get('/profile', authenticate, (req, res) => {
res.json(req.user);
});
app.listen(3000, () => console.log("Server running on port 3000"));
ポイント
- ユーザーが
/login
にアクセスすると、JWT を発行 する。 /profile
などの保護されたエンドポイントにアクセスする際は、JWT をAuthorization: Bearer <token>
として送信 する。- JWT はサーバー側で状態を持たない(ステートレス)ため、Redis などの外部ストレージを使わず、スケールしやすい。
Hybrid Approach(JWT + Redis)によるバランス型アプローチ
概要
- JWT を使って基本的な認証を行い、Redis にブラックリスト管理を行うことでトークンの無効化を実現。
- ログアウト時やトークン無効化時に、Redis に保存する仕組みを導入することで JWT の欠点を補う。
メリット
- JWT の スケーラビリティを活かしつつ、トークンの無効化が可能。
- Redis の セッション管理の高速性を活かしつつ、全リクエストでの Redis 依存を減らせる。
デメリット
- Redis の追加管理が必要。
- ログアウト処理が増えるため、実装が複雑になる。
import express from 'express';
import jwt from 'jsonwebtoken';
import Redis from 'ioredis';
const app = express();
const SECRET_KEY = "your_secret_key"; // 環境変数で管理
const redisClient = new Redis();
app.use(express.json());
app.post('/login', (req, res) => {
const user = { id: 1, name: "John Doe" };
const token = jwt.sign(user, SECRET_KEY, { expiresIn: '1h' });
res.json({ token });
});
const authenticate = async (req, res, next) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(401).send("Unauthorized");
const isRevoked = await redisClient.get(`blacklist:${token}`);
if (isRevoked) return res.status(403).send("Token has been revoked");
try {
req.user = jwt.verify(token, SECRET_KEY);
next();
} catch (err) {
res.status(403).send("Invalid token");
}
};
app.post('/logout', async (req, res) => {
const token = req.headers.authorization?.split(" ")[1];
if (!token) return res.status(400).send("Token required");
await redisClient.setex(`blacklist:${token}`, 3600, "revoked"); // 1時間後に自動削除
res.send("Logged out");
});
app.get('/profile', authenticate, (req, res) => {
res.json(req.user);
});
app.listen(3000, () => console.log("Server running on port 3000"));
ポイント
- JWT を使用した認証
- ユーザーが /login すると JWT を発行し、クライアントに渡す。
- クライアントは以降のリクエストで JWT を Authorization ヘッダーに含めて送信。
- サーバーは JWT を検証し、正当なリクエストかを判断。
- Redis を活用したトークンの無効化
- /logout でログアウト処理を行うと、JWT を Redis のブラックリストに追加。
- /profile などの保護されたエンドポイントでは、ブラックリストに含まれる JWT を拒否。
この方式により、JWT の課題である「トークンの無効化が難しい問題」を解決 しています。
キャッシュの TTL(Time To Live)の適切な設定
TTL(Time To Live)は、キャッシュデータの有効期間を制御する重要な概念であり、適切に設定することでパフォーマンス向上やコスト削減を実現できます。一方で、適切な TTL 設定をしないと、古いデータの配信やキャッシュヒット率の低下につながるため、データの特性に応じた最適な値を選択することが重要です。
TTL 設定の考え方
キャッシュの TTL を決める際には、以下のポイントを考慮します。
- データの更新頻度
- 頻繁に更新されるデータは TTL を短く設定することで、新鮮なデータを保持できる。
- 変更が少ないデータは長い TTL を設定し、不要なキャッシュミスを減らす。
- データの重要度
- 認証情報やリアルタイムデータなどのクリティカルな情報は短い TTL を設定。
- 静的リソースや履歴データのような不変データは長い TTL を設定。
- パフォーマンスとコストのバランス
- TTL を長くするとキャッシュヒット率が向上し、API や DB への負荷を軽減できる。
- ただし、TTL が長すぎると古いデータを保持し続けるリスクがある。
データ種別ごとの推奨 TTL
データ種別 | 推奨 TTL | 理由 |
---|---|---|
静的コンテンツ(CSS, JS, 画像) | 数時間〜数日 | 頻繁な更新がないため、長めに設定して負荷を軽減 |
ユーザープロフィール情報 | 数分〜数時間 | 変更は比較的少ないが、リアルタイム性も考慮する必要がある |
API レスポンス(頻繁に更新されるもの) | 数秒〜数分 | 最新の情報を反映する必要があるため短めに設定 |
認証トークン(JWT など) | 数時間〜1日 | セキュリティと利便性のバランスを考慮して設定 |
TTL を動的に調整する方法
TTL は固定値として設定することもできますが、柔軟に調整することでより適切なキャッシュ戦略を構築できます。
主な手法として3種類挙げられます。
- スライディングエクスパイア(Sliding Expiration)
- イベントベースのキャッシュクリア(Cache Invalidation)
- CDN のキャッシュ無効化 API を活用
- スライディングエクスパイア(Sliding Expiration)
- アクセスがあるたびに TTL を延長する方式。
- キャッシュされたデータにアクセスがあると TTL をリセットし、一定時間経過するまでキャッシュを保持する。
- セッション管理や一時的なデータに適している。
メリット
- 頻繁に使われるデータはキャッシュに残り続ける
- 例えば、ログインしているユーザーのセッションデータをキャッシュする場合、アクティブなユーザーはずっとキャッシュを保持できる。
- 不要なデータのキャッシュを削除できる
- しばらくアクセスのなかったデータは、自動的にキャッシュが消えるため、ストレージを節約できる。
デメリット
- キャッシュの管理が複雑になる
- アクセスのたびに TTL を延長するため、通常の TTL 方式よりもロジックが増える。
- 特定のデータがキャッシュに長期間残る可能性がある
- アクセスが途絶えない限り、キャッシュがクリアされないため、古いデータが残るリスクがある。
例)ユーザーセッション管理
ログインしたユーザーのセッション情報をキャッシュに保存するとします。
- アクセスが続く限りセッション情報はキャッシュに残る
- 一定時間アクセスがなければセッションが削除される
const cacheKey = `session_${userId}`;
const sessionTTL = 600; // 10 分
// 1. ユーザーのセッションをキャッシュから取得
const sessionData = cache.get(cacheKey);
if (sessionData) {
// 2. アクセスがあったので TTL をリセット(キャッシュ延命)
cache.set(cacheKey, sessionData, sessionTTL);
return sessionData;
}
// 3. セッションがなければ、新しいセッションを作成
const newSession = createSession(userId);
cache.set(cacheKey, newSession, sessionTTL);
return newSession;
- これにより、ユーザーがアクティブにアクセスしている間はセッションが保持される。
- ユーザーが 10 分間アクセスしなかった場合、キャッシュが削除され、再ログインが必要になる。
例)API レスポンスのキャッシュ
検索結果のキャッシュを考えてみます。
const cacheKey = `search_results_${query}`;
const cacheTTL = 300; // 5 分
// キャッシュを取得
const cachedResults = cache.get(cacheKey);
if (cachedResults) {
// キャッシュがあれば、TTL を延長
cache.set(cacheKey, cachedResults, cacheTTL);
return cachedResults;
}
// キャッシュがない場合は API からデータを取得
const results = fetchSearchResults(query);
cache.set(cacheKey, results, cacheTTL);
return results;
- 検索結果のキャッシュが5 分間維持される。
- もしユーザーが3 分後に同じ検索をしたら、さらに 5 分延長される。
- しかし、5 分間アクセスがなければキャッシュは削除され、次回の検索時に新しいデータが取得される。
- イベントベースのキャッシュクリア(Cache Invalidation)
- データの更新が発生した際に、関連するキャッシュを削除する方式。
- API や DB のデータ更新時に該当キャッシュを破棄することで、常に最新のデータを提供可能。
通常、キャッシュには TTL(Time To Live) を設定して、一定時間経過後に自動的に削除されるようにします。しかし、データが変更された場合、TTL の期限が切れるまで 古いデータが残り続ける 可能性があります。
例えば:
- ユーザーがプロフィールを更新したのに、キャッシュが古いままで反映されない
- 商品の在庫が変わったのに、キャッシュされた情報が古くて誤った在庫数を表示する
- 通知システムで最新の通知を受け取るべきなのに、キャッシュのせいで古い通知が表示される
こうした問題を解決するために、データが変更されたら、そのタイミングで関連するキャッシュを削除する(無効化する) という仕組みを導入します。
function updateUserProfile(userId, newProfileData) {
// 1. データベースを更新
database.updateUserProfile(userId, newProfileData);
// 2. 変更があったのでキャッシュを削除
cache.delete(`user_profile_${userId}`);
}
キャッシュクリアのベストプラクティス
- 変更が発生したデータに関連するキャッシュのみ削除する
- 必要以上にキャッシュを削除すると、キャッシュヒット率が下がるため注意。
- キャッシュ削除後、すぐに新しいデータをキャッシュする(プリフェッチ)
- 削除した後に即座に新しいデータを取得&キャッシュすることで、キャッシュミスを減らす。
- タグベースのキャッシュ管理を活用する
- 例: category_123 というタグを持つキャッシュを一括削除することで、関連データの削除を簡単に管理。
- CDN のキャッシュ無効化 API を活用
- CDN(Content Delivery Network)を利用している場合、CDN のキャッシュを明示的に削除することで即座に最新データを提供可能。
- Cloudflare、AWS CloudFront、Fastly などでは、API を使ってキャッシュを無効化できる。
例(Cloudflare のキャッシュ削除 API の利用)
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/purge_cache" \
-H "Authorization: Bearer {api_token}" \
-H "Content-Type: application/json" \
--data '{"purge_everything":true}'
適用例としては下記が挙げられます。
- ブログ記事やニュース記事の即時更新
- EC サイトの商品情報の更新
- マーケティングキャンペーンページの更新
TTL 設定のベストプラクティス
- デフォルトの TTL を適切に設定する
- 変更頻度の低いデータは長めに設定し、動的データは短めに設定。
- ユーザー体験とパフォーマンスのバランスを取る
- キャッシュが短すぎると毎回データを取得するため負荷が増加。
- キャッシュが長すぎると古いデータを提供してしまう可能性がある。
- TTL をデータごとにカスタマイズする
- すべてのデータに一律の TTL を設定するのではなく、データの特性に応じて適切な値を設定。
- キャッシュクリアの仕組みを適切に設計する
- データ変更時に明示的にキャッシュを削除する仕組みを導入する。
コンテンツのプリフェッチ(Next.js ISR / SSG の活用)
Next.js では、静的サイト生成(SSG)やインクリメンタル静的再生成(ISR)を利用することで、Webページの表示速度を最適化しつつ、サーバー負荷を軽減できます。これにより、ユーザーは高速なページ読み込みを体験でき、開発者は効率的なコンテンツ配信を実現できます。
SSG(Static Site Generation)とは?
SSG は、ビルド時にあらかじめ HTML を生成し、それを CDN(Content Delivery Network)から配信する 仕組みです。データが頻繁に変わらないページに適しており、以下のようなメリットがあります。
SSG の特徴
- ビルド時 (next build) に getStaticProps を使ってデータを取得
- 生成された HTML が CDN にキャッシュされ、高速に配信される
- サーバー負荷が低く、スケーラビリティが高い
- ページの内容が変更された場合、再デプロイが必要
SSG の適用例
- ブログ記事(記事の内容は頻繁に更新されない)
- ドキュメントサイト(静的な情報が中心)
- 製品ページ(数日〜数週間ごとに更新される商品情報)
SSG の実装例
getStaticProps
を使用して SSG を実現できます。
export async function getStaticProps() {
const res = await fetch('https://api.example.com/posts');
const posts = await res.json();
return {
props: { posts }, // ビルド時にデータを取得し、ページに渡す
};
}
API からデータを取得し、ビルド時に静的 HTML として保存します。
ISR(Incremental Static Regeneration)とは?
ISR は SSG の拡張版 で、ページを静的に生成しつつ、一定時間ごとに最新データを反映できる 機能です。これにより、サイトの更新性とパフォーマンスのバランスをとることができます。
-
ISR の特徴
- getStaticProps に revalidate オプションを指定することで、一定時間後にページを再生成
- ビルド後も静的ページを最新の状態に保てる
- CDN キャッシュのメリットを活かしつつ、リアルタイム性も確保
- 完全な SSR(サーバーサイドレンダリング)ほどの負荷をかけずに、動的なデータを扱える
-
ISR の適用例
- ニュースサイト(記事が一定時間ごとに更新される)
- ECサイトの商品ページ(在庫数や価格が変更される)
- ランキングやトレンドを表示するページ
-
ISR の実装例
Next.js では、getStaticProps に revalidate を設定するだけで ISR を有効化できます。
export async function getStaticProps() {
const res = await fetch('https://api.example.com/products');
const products = await res.json();
return {
props: { products },
revalidate: 60, // 60秒ごとにページを再生成
};
}
ページが 60 秒ごとに最新データに更新 されるため、リアルタイム性が求められるコンテンツに適しています。
ISR の仕組み
-
最初のリクエスト
初回リクエスト時に Next.js が getStaticProps を実行し、ページを生成
そのページが CDN にキャッシュ され、すべてのユーザーに提供される -
再生成のタイミング
revalidate の時間が経過すると、新しいリクエストが来たときにバックグラウンドで再生成が行われる
新しいリクエストは古いページを受け取り、新しいページが完成したら以降のリクエストに適用
SSG / ISR の活用によるパフォーマンス向上
Next.js の SSG や ISR を適切に活用することで、以下のようなメリットを得られます。
- ページの読み込み速度を高速化
静的ページを CDN から配信することで、表示が速くなる - サーバー負荷を軽減
リクエストのたびに API を呼ぶ SSR と比べて、処理負荷を大幅に減らせる - SEO(検索エンジン最適化)に有利
事前に HTML を生成するため、クローラーがコンテンツを適切に評価できる - リアルタイム性を確保しつつ、高速な配信を実現
ISR を使うことで、更新頻度の高いデータも適切に配信できる
クライアントサイドキャッシュの最適化(Service Worker の適用)
Service Worker は、ブラウザとネットワークの間に存在するスクリプトで、以下のような特徴を持ちます。
- バックグラウンドで動作 し、ページの再読み込みとは独立して機能する
- ネットワークリクエストをインターセプト(傍受)し、キャッシュや API リクエストを管理できる
- PWA(プログレッシブウェブアプリ)の必須技術 であり、オフラインサポートや高速化を実現する
今回はReact で Service Worker を適用する方法を Workbox を活用しつつご紹介します。
Workbox をインストール
npm install workbox-window
プロジェクトの public/
ディレクトリに serviceWorker.js
を作成し、Service Worker の登録やキャッシュ戦略を設定します。
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { StaleWhileRevalidate, CacheFirst, NetworkFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
// ビルド時にキャッシュするリソース
precacheAndRoute(self.__WB_MANIFEST || []);
// 静的リソース(CSS, JS, 画像)をキャッシュ
registerRoute(
({ request }) => request.destination === 'style' || request.destination === 'script' || request.destination === 'image',
new CacheFirst({
cacheName: 'static-resources',
plugins: [
new ExpirationPlugin({
maxEntries: 50, // キャッシュの上限数
maxAgeSeconds: 30 * 24 * 60 * 60, // 30日間保持
}),
],
})
);
// API レスポンスのキャッシュ(stale-while-revalidate)
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new StaleWhileRevalidate({
cacheName: 'api-cache',
})
);
// 古いキャッシュを削除
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.filter((cacheName) => cacheName !== 'static-resources' && cacheName !== 'api-cache')
.map((cacheName) => caches.delete(cacheName))
);
})
);
});
React のエントリーポイント src/main.tsx
で Service Worker を登録します。
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { registerSW } from './serviceWorkerRegistration';
const root = ReactDOM.createRoot(document.getElementById('root')!);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// Service Worker を登録
registerSW();
React で Service Worker を管理するための serviceWorkerRegistration.ts
を作成します。
import { Workbox } from 'workbox-window';
export function registerSW() {
if ('serviceWorker' in navigator) {
const wb = new Workbox('/serviceWorker.js');
wb.addEventListener('installed', (event) => {
if (event.isUpdate) {
console.log('新しいバージョンの Service Worker がインストールされました。');
}
});
wb.addEventListener('waiting', () => {
console.log('Service Worker が待機中です。');
});
wb.addEventListener('activated', () => {
console.log('Service Worker がアクティブになりました。');
});
wb.register();
}
}
キャッシュを手動で削除するコンポーネントを作成します。
import React from 'react';
const ClearCacheButton: React.FC = () => {
const clearCache = async () => {
if ('caches' in window) {
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map((name) => caches.delete(name)));
alert('キャッシュが削除されました!');
}
};
return <button onClick={clearCache}>キャッシュをクリア</button>;
};
export default ClearCacheButton;
このボタンを App.tsx
に追加すると、キャッシュを削除できます。
import React from 'react';
import ClearCacheButton from './components/ClearCacheButton';
const App: React.FC = () => {
return (
<div>
<h1>React + Service Worker + Workbox</h1>
<ClearCacheButton />
</div>
);
};
export default App;
- キャッシュの更新が反映されない問題を防げる
- 開発者がデバッグしやすくなる
- 特定のデータだけをクリアできる
- 画像のキャッシュだけクリアして、API のレスポンスは保持する
- 特定の API のレスポンスだけクリアする
- 重要なデータ(認証トークンなど)は消さずに、CSS や JS のキャッシュだけ削除
などの対応でキャッシュクリアボタンを実装してみるのもいいかと思います。
適切なキャッシュポリシーの設定
stale-while-revalidate
(キャッシュ優先 + バックグラウンド更新)
- 仕組み: キャッシュから即座にレスポンスを返しつつ、バックグラウンドで最新データを取得
- 適用例: API レスポンス、CDN から取得する静的ファイル
import { StaleWhileRevalidate } from 'workbox-strategies';
registerRoute(
({ request }) => request.destination === 'script' || request.destination === 'style',
new StaleWhileRevalidate({
cacheName: 'static-resources',
})
);
メリット: ページの表示速度が速く、かつデータが自動更新される。
network-first
(ネットワーク優先 + フォールバックキャッシュ)
- 仕組み: ネットワークからデータを取得し、失敗時はキャッシュから提供
- 適用例: ユーザーが最新の情報を必要とする API データ
import { NetworkFirst } from 'workbox-strategies';
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api-cache',
networkTimeoutSeconds: 5, // 5秒以内に応答がなければキャッシュを使用
})
);
メリット: 可能な限り最新データを取得しつつ、オフライン時にもデータを表示できる。
cache-first
(キャッシュ優先 + フォールバックネットワーク)
- 仕組み: キャッシュを優先して使用し、キャッシュがない場合はネットワークから取得
- 適用例: 変更が少ないリソース(画像、フォント、CSS)
import { CacheFirst } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'image-cache',
plugins: [
new ExpirationPlugin({
maxEntries: 50, // キャッシュする最大エントリ数
maxAgeSeconds: 30 * 24 * 60 * 60, // 30日間有効
}),
],
})
);
メリット: サイトの表示速度を優先
キャッシュの自動クリアルールの策定
キャッシュ管理は、パフォーマンスの最適化と最新データの提供のバランスを取るために重要です。適切なルールを設定することで、不要なキャッシュの蓄積を防ぎ、効率的なデータ配信を実現できます。
キャッシュの期限管理
キャッシュの保持期間を適切に設定することで、無駄なキャッシュを防ぎつつ、最新データをユーザーに届けることができます。
-
Cache-Control ヘッダーの設定
max-age=<秒数>
:指定秒数の間、キャッシュを有効にするmust-revalidate
:期限が切れたキャッシュは必ず再検証するno-cache
:キャッシュされるが、利用前に必ずサーバーと整合性を確認するno-store
:キャッシュを完全に無効化
-
ETag の活用
- サーバー側でコンテンツのハッシュを生成し、変更がない場合はキャッシュを再利用
- クライアントは If-None-Match を送信し、変更がなければ 304 Not Modified を受け取る
バージョニングを活用
キャッシュのクリアを適切に行うため、アセットの URL にバージョン情報を組み込む方法が有効です。
-
ファイル名にバージョンを付与
-
変更があった際に異なるURLを提供し、古いキャッシュを参照させない
- 例:
- /static/js/app.v1.2.3.js
- /static/css/style.v2.1.0.css
- 例:
-
CDN のキャッシュクリアAPIを活用し、新しいバージョンを即座に反映
- 例: Cloudflare の cache purge 機能を利用
クエリキャッシュの導入(React Query / Apollo Client の活用)
React アプリケーションにおいて、データフェッチとキャッシュ管理を効率化するには、React Query または Apollo Client を活用するのが有効です。これにより、ネットワークリクエストの最適化 や パフォーマンス向上 を実現できます。
React Query の活用
React Query は、REST API や GraphQL を含むあらゆるデータ取得のキャッシュ管理を簡単に行えるライブラリです。
useQuery
を利用したデータフェッチ
React Query のuseQuery
フックを使うことで、データ取得とキャッシュ管理を自動化できます。
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
const fetchData = async () => {
const { data } = await axios.get('https://jsonplaceholder.typicode.com/posts');
return data;
};
const Posts = () => {
const { data, error, isLoading } = useQuery({
queryKey: ['posts'], // クエリの識別キー(キャッシュ管理に使用)
queryFn: fetchData, // データ取得関数
staleTime: 1000 * 60, // 1分間は「フレッシュ」なデータとみなす
cacheTime: 1000 * 300 // 5分間キャッシュを保持
});
if (isLoading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.map((post: { id: number; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
};
export default Posts;
staleTime
とcacheTime
の調整
staleTime
: キャッシュが 古いとみなされるまでの時間。この時間内なら、再フェッチせずキャッシュを使用。cacheTime
: キャッシュが メモリに残る時間。この時間を過ぎるとキャッシュが削除される。
- invalidateQueries でキャッシュを手動更新
データの変更があった際に、特定のクエリを無効化し、最新データを取得できます。
import { useQuery, useQueryClient } from '@tanstack/react-query';
import axios from 'axios';
const fetchPosts = async () => {
const { data } = await axios.get('https://jsonplaceholder.typicode.com/posts');
return data;
};
const Posts = () => {
const queryClient = useQueryClient();
const { data, isLoading } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
const refreshData = () => {
queryClient.invalidateQueries({ queryKey: ['posts'] }); // キャッシュを無効化し、データを再取得
};
if (isLoading) return <p>Loading...</p>;
return (
<div>
<button onClick={refreshData}>更新</button>
<ul>
{data.map((post: { id: number; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
};
export default Posts;
Apollo Client の活用
Apollo Client は GraphQL API に特化したデータ管理ライブラリで、InMemoryCache を活用したクエリキャッシュ が強力です。
Apollo Client のセットアップ
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
const client = new ApolloClient({
uri: 'https://your-graphql-endpoint.com/graphql',
cache: new InMemoryCache(),
});
const App = () => (
<ApolloProvider client={client}>
<YourComponent />
</ApolloProvider>
);
export default App;
useQuery
を利用したデータフェッチ
import { gql, useQuery } from '@apollo/client';
const GET_POSTS = gql`
query GetPosts {
posts {
id
title
}
}
`;
const Posts = () => {
const { data, loading, error } = useQuery(GET_POSTS, {
fetchPolicy: 'cache-first', // キャッシュを優先して使用
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.posts.map((post: { id: number; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
};
export default Posts;
Apollo Client では fetchPolicy
を指定することで、データ取得の挙動を制御できます。
fetchPolicy | 説明 |
---|---|
cache-first | キャッシュがあれば使用、なければネットワークから取得 |
network-only | 常にネットワークから取得 |
cache-and-network | キャッシュを即座に使用しつつ、ネットワークでも取得 |
no-cache | キャッシュを使用せず、都度ネットワークから取得 |
GraphQL ミューテーション後に refetchQueries
を使うことで、特定のデータを最新の状態にできます。
import { gql, useMutation } from '@apollo/client';
const CREATE_POST = gql`
mutation CreatePost($title: String!) {
createPost(title: $title) {
id
title
}
}
`;
const GET_POSTS = gql`
query GetPosts {
posts {
id
title
}
}
`;
const CreatePost = () => {
const [createPost] = useMutation(CREATE_POST, {
refetchQueries: [{ query: GET_POSTS }], // クエリを再取得
});
const handleCreate = async () => {
await createPost({ variables: { title: 'New Post' } });
};
return <button onClick={handleCreate}>新規投稿</button>;
};
export default CreatePost;
React Query と Apollo Client
項目 | React Query | Apollo Client |
---|---|---|
主な用途 | REST API / GraphQL | GraphQL API |
キャッシュ管理 | useQuery , cacheTime , staleTime |
InMemoryCache |
データ更新 | invalidateQueries |
refetchQueries |
データ取得制御 | ` | fetchPolicy (cache-first など) |
キャッシュヒット率の分析と最適化
キャッシュ戦略を適用した後、その効果を定量的に評価し、最適化を行うことが重要です。ここでは、キャッシュヒット率の計測方法について詳しく解説します。
クライアント側の計測
キャッシュヒット率(Cache Hit Ratio)は、「キャッシュから取得できたリクエストの割合」を示す指標です。キャッシュの有効性を確認し、最適な設定を行うために、以下の方法で測定します。
- ブラウザ・Service Worker を利用した計測
- ブラウザのPerformance APIを活用
performance.getEntriesByType("resource")
を使用し、リソースがキャッシュから取得されたかを確認。transferSize === 0
のリソースはキャッシュヒットと判断できる。
performance.getEntriesByType("resource").forEach((entry) => {
console.log(entry.name, entry.transferSize === 0 ? "Cache Hit" : "Cache Miss");
});
- Service Worker のログを分析
- Service Worker で fetch イベントをフックし、
cache.match(event.request)
の結果をロギング。 - Cache Hit したリクエストと Cache Miss のリクエストを比較。
- Service Worker で fetch イベントをフックし、
self.addEventListener("fetch", (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
if (cachedResponse) {
console.log("Cache Hit:", event.request.url);
return cachedResponse;
} else {
console.log("Cache Miss:", event.request.url);
return fetch(event.request);
}
})
);
});
- GraphQL のキャッシュヒット率計測(Apollo Client)
cache.readQuery()
を利用し、クエリのキャッシュヒットをチェック。
const { cache } = useApolloClient();
const data = cache.readQuery({
query: GET_USER_PROFILE,
variables: { id: "123" },
});
console.log(data ? "Cache Hit" : "Cache Miss");
Apollo DevTools
を使用し、cache
タブからどのデータがキャッシュに保持されているかを確認。useQuery
のfetchPolicy
を "cache-first" に設定し、キャッシュヒット時のレスポンス速度を測定。
サーバー側でのキャッシュヒット率の集計
クライアント側だけでなく、サーバーやCDNでキャッシュヒット率をログに記録し、集計することで、システム全体の傾向を把握できます。
- Nginx や Apache のログを解析
- Nginx の $upstream_cache_status をログに記録し、キャッシュヒット率を集計。
- HIT/MISS/BYPASS の割合を分析し、キャッシュ設定を最適化。
log_format cache_status '$remote_addr - $upstream_cache_status - $request';
access_log /var/log/nginx/cache.log cache_status;
- GraphQL や API レイヤーでキャッシュ状況を記録
- Apollo Server や REST API で、キャッシュヒット(
cache.get()
)の回数を記録。 - Redis やメモリキャッシュ(
LruCache
)のヒット率を Datadog などで監視。
const redis = require("redis");
const client = redis.createClient();
const cacheKey = `user:${userId}`;
client.get(cacheKey, (err, data) => {
if (data) {
console.log("Cache Hit");
} else {
console.log("Cache Miss");
}
});
- CDN(Cloudflare, AWS CloudFront)のログを分析
Cloudflare
のキャッシュヒット率はcf-cache-status
ヘッダーで確認可能。AWS CloudFront
のx-cache
ヘッダーで HIT/MISS を記録し、ログを集計。
さいごに
キャッシュを適切に設計・運用することで、アプリケーションのパフォーマンスは飛躍的に向上します。CDNによる静的コンテンツの配信最適化、データベース負荷軽減のためのRedis / Memcached、APIレスポンスキャッシュの活用、Next.js ISR / SSGのプリフェッチ、クエリキャッシュの導入など、それぞれの手法にはメリットと適用シナリオがあります。
しかし、キャッシュには適切なTTL設定や自動クリアルールの策定が欠かせません。過剰なキャッシュはデータの整合性を損なう可能性があり、逆にキャッシュ不足はパフォーマンス低下を招きます。キャッシュヒット率の分析やモニタリングを行いながら、最適なキャッシュ戦略を構築しましょう。
本記事が、あなたのアプリケーションのパフォーマンス改善とスケーラビリティ向上の一助となれば幸いです。
関連する技術ブログ
GraphQL・REST API の堅牢な認可設計:RBAC・ABAC・OAuth 2.0 のベストプラクティス
GraphQL & REST API の堅牢な認可設計を構築する方法とは?RBAC・ABAC の活用、Rate Limiting、API Gateway、監視のベストプラクティスをまとめました。
shinagawa-web.com
自動化で業務効率を最大化する方法
定型作業をスクリプトや Bot に置き換え、自動化することで作業時間を削減。データ処理やデプロイ、アラート対応の自動化により、開発生産性を向上させます。
shinagawa-web.com
フロントエンド開発に役立つモックサーバー構築:@graphql-tools/mock と Faker を使った実践ガイド
フロントエンド開発を先行させるために、バックエンドが未完成でもモックサーバーを立ち上げる方法を解説。@graphql-tools/mock と Faker を使用して、実際のデータに近い動作をシミュレートします。
shinagawa-web.com
フロントエンドテスト戦略の最適解:ユニットテストからE2Eまで徹底強化する方法
est / Vitest を活用したユニットテストの導入、React Testing Library でのコンポーネントテスト最適化、Playwright / Cypress による E2E テストの拡充、API テストの自動化など、開発の品質と効率を向上させるテスト戦略を解説。CI/CD でのテスト実行最適化や、依存関係を考慮したテスト設計、テストカバレッジの可視化、フィーチャーフラグを考慮した戦略まで、実践的なノウハウを詳しく紹介します。
shinagawa-web.com
Apollo Serverで始めるGraphQL入門:Express & MongoDB連携ガイド
GraphQLとApollo Serverの基本から、MongoDBを活用したデータ操作までを詳しく解説。Expressを使用した効率的なサーバー構築方法を学びましょう。初心者にもわかりやすいハンズオン形式でお届けします。
shinagawa-web.com
ExpressとMongoDBで簡単にWeb APIを構築する方法【TypeScript対応】
本記事では、MongoDB Atlasを活用してREST APIを構築する方法を、初心者の方にも分かりやすいステップで解説します。プロジェクトの初期設定からMongoDBの接続設定、Expressを使用したルートの作成、さらにTypeScriptを用いた型安全な実装まで、実践的なサンプルコードを交えて丁寧に説明します。
shinagawa-web.com
Express(+TypeScript)入門ガイド: Webアプリケーションを素早く構築する方法
Node.jsでWebアプリケーションを構築するための軽量フレームワーク、Expressの基本的な使い方を解説。シンプルなサーバー設定からルーティング、ミドルウェアの活用方法、TypeScriptでの開発環境構築まで、実践的なコード例とともに学べます。
shinagawa-web.com
React + Express を活用したモノレポ環境構築ガイド:Turborepoで効率的な開発を実現
React、Expressを使ってモノレポ環境を構築し、Turborepoでの効率的な開発を実現する方法を解説します。フロントエンドとバックエンドを統合した最適な開発環境を整えるためのステップを順を追って紹介します。
shinagawa-web.com