メインコンテンツまでスキップ

[Medium] 📄 HTTP Caching

1. What is HTTP caching and why is it important?​

HTTP キャッシュとは何か?なぜ重要なのか?

HTTP キャッシュとは、クライアント(ブラウザ)や中間サーバーに HTTP レスポンスを一時的に保存する技術です。後続のリクエスト時にキャッシュされたデータを直接使用でき、サーバーに再度リクエストする必要がなくなります。

キャッシュ vs 一時保存:何が違うのか?​

技術文書では、この2つの用語がしばしば混同されますが、実際には異なる意味を持っています。

Cache(キャッシュ)​

定義:パフォーマンス最適化のために保存されるデータのコピーで、「再利用」と「アクセスの高速化」を重視します。

特徴:

  • ✅ パフォーマンス向上が目的
  • ✅ データを繰り返し使用可能
  • ✅ 明確な有効期限ポリシーがある
  • ✅ 通常は元データのコピー

例:

// HTTP Cache - API レスポンスのキャッシュ
Cache-Control: max-age=3600 // 1時間キャッシュ

// Memory Cache - 計算結果のキャッシュ
const cache = new Map();
function fibonacci(n) {
if (cache.has(n)) return cache.get(n); // キャッシュを再利用
const result = /* 計算 */;
cache.set(n, result);
return result;
}

Temporary Storage(一時保存)​

定義:一時的に保存されるデータで、「一時性」と「削除される」ことを重視します。

特徴:

  • ✅ 一時的な保存が目的
  • ✅ 必ずしも再利用されるとは限らない
  • ✅ ライフサイクルが通常短い
  • ✅ 中間状態を含む場合がある

例:

// sessionStorage - ユーザー入力の一時保存
sessionStorage.setItem('formData', JSON.stringify(form)); // タブを閉じると削除

// ファイルアップロードの一時保存
const tempFile = await uploadToTemp(file); // 処理後に削除
await processFile(tempFile);
await deleteTempFile(tempFile);

比較表​

特性Cache(キャッシュ)Temporary Storage(一時保存)
主な目的パフォーマンス最適化一時的な保存
再利用はい、複数回の読み取り必ずしもそうではない
ライフサイクルポリシーに基づく通常は短い
典型的な用途HTTP Cache, Memory CachesessionStorage, 一時ファイル
英語対応CacheTemp / Temporary / Buffer

実際の使用における違い​

// ===== Cache(キャッシュ)の使用シーン =====

// 1. HTTP キャッシュ:API レスポンスの再利用
fetch('/api/users') // 初回リクエスト
.then((response) => response.json());

fetch('/api/users') // 2回目はキャッシュから読み取り
.then((response) => response.json());

// 2. 計算結果のキャッシュ
const memoize = (fn) => {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key); // 再利用
const result = fn(...args);
cache.set(key, result);
return result;
};
};

// ===== Temporary Storage(一時保存)の使用シーン =====

// 1. フォームデータの一時保存(誤って閉じることを防止)
window.addEventListener('beforeunload', () => {
sessionStorage.setItem('formDraft', JSON.stringify(formData));
});

// 2. アップロードファイルの一時保存
async function handleUpload(file) {
const tempPath = await uploadToTempStorage(file); // 一時保存
const processed = await processFile(tempPath);
await deleteTempFile(tempPath); // 使用後に削除
return processed;
}

// 3. 中間計算結果の一時保存
const tempResults = []; // 中間結果を一時保存
for (const item of items) {
tempResults.push(process(item));
}
const final = combine(tempResults); // 使用後は不要

Web 開発での応用​

// HTTP Cache(キャッシュ)- 長期保存、繰り返し使用
Cache-Control: public, max-age=31536000, immutable
// → ブラウザはこのファイルを1年間キャッシュし、繰り返し使用

// sessionStorage(一時保存)- 一時的な保存、閉じたら削除
sessionStorage.setItem('tempData', data);
// → 現在のタブでのみ有効、閉じると削除

// localStorage(長期保存)- 両者の中間
localStorage.setItem('userPreferences', prefs);
// → 永続的に保存されるが、パフォーマンス最適化のためではない

なぜこの2つの概念を区別することが重要なのか?​

  1. 設計上の判断:

    • パフォーマンス最適化が必要? → キャッシュを使用
    • 一時的な保存が必要? → 一時保存を使用
  2. リソース管理:

    • キャッシュ:ヒット率、有効期限ポリシーを重視
    • 一時保存:クリーンアップのタイミング、容量制限を重視
  3. 面接での回答:

    • 「パフォーマンスをどう最適化するか」→ キャッシュ戦略について話す
    • 「一時データをどう扱うか」→ 一時保存の方法について話す

本記事では、主に Cache(キャッシュ)、特に HTTP キャッシュの仕組みについて説明します。

キャッシュのメリット​

  1. ネットワークリクエストの削減:ローカルキャッシュから直接読み取り、HTTP リクエストの送信が不要
  2. サーバー負荷の軽減:サーバーが処理するリクエスト数を削減
  3. ページ読み込み速度の向上:ローカルキャッシュの読み取り速度はネットワークリクエストよりはるかに速い
  4. 帯域幅の節約:データ転送量を削減
  5. ユーザー体験の改善:ページの応答が速く、よりスムーズに使用できる

キャッシュの種類​

┌─────────────────────────────────────┐
│ ブラウザキャッシュの階層 │
├─────────────────────────────────────┤
│ 1. Memory Cache(メモリキャッシュ) │
│ - 最速、容量は小さい │
│ - タブを閉じると削除 │
├─────────────────────────────────────┤
│ 2. Disk Cache(ディスクキャッシュ) │
│ - より遅い、容量は大きい │
│ - 永続的に保存 │
├─────────────────────────────────────┤
│ 3. Service Worker Cache │
│ - 開発者が完全にコントロール │
│ - オフラインアプリのサポート │
└─────────────────────────────────────┘

2. What are the HTTP caching strategies?​

HTTP キャッシュ戦略にはどのようなものがあるか?

キャッシュ戦略の分類​

HTTP キャッシュ戦略
├── 強キャッシュ (Strong Cache)
│ ├── Cache-Control
│ └── Expires
└── ネゴシエーションキャッシュ (Negotiation Cache)
├── Last-Modified / If-Modified-Since
└── ETag / If-None-Match

1. 強キャッシュ(Strong Cache / Fresh)​

特徴:ブラウザがローカルキャッシュから直接読み取り、サーバーへのリクエストを送信しません。

Cache-Control(HTTP/1.1)​

Cache-Control: max-age=3600

よく使用されるディレクティブ:

// 1. max-age:キャッシュの有効時間(秒)
Cache-Control: max-age=3600 // 1時間キャッシュ

// 2. no-cache:サーバーへの検証が必要(ネゴシエーションキャッシュを使用)
Cache-Control: no-cache

// 3. no-store:まったくキャッシュしない
Cache-Control: no-store

// 4. public:どのキャッシュでも保存可能(ブラウザ、CDN)
Cache-Control: public, max-age=31536000

// 5. private:ブラウザのみキャッシュ可能
Cache-Control: private, max-age=3600

// 6. immutable:リソースは変更されない(hash ファイル名と併用)
Cache-Control: public, max-age=31536000, immutable

// 7. must-revalidate:期限切れ後はサーバーで検証が必要
Cache-Control: max-age=3600, must-revalidate

Expires(HTTP/1.0、非推奨)​

Expires: Wed, 21 Oct 2025 07:28:00 GMT

問題点:

  • 絶対時間を使用し、クライアント時間に依存する
  • クライアント時間が不正確だとキャッシュが正しく動作しない
  • Cache-Control に置き換えられた

2. ネゴシエーションキャッシュ(Negotiation Cache / Validation)​

特徴:ブラウザがサーバーにリクエストを送信し、リソースが更新されたかどうかを確認します。

Last-Modified / If-Modified-Since​

# サーバーレスポンス(初回リクエスト)
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT

# ブラウザリクエスト(後続リクエスト)
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT

フロー:

  1. 初回リクエスト:サーバーが Last-Modified を返す
  2. 後続リクエスト:ブラウザが If-Modified-Since を付与
  3. リソース未変更:サーバーが 304 Not Modified を返す
  4. リソース変更済み:サーバーが 200 OK と新しいリソースを返す

ETag / If-None-Match​

# サーバーレスポンス(初回リクエスト)
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# ブラウザリクエスト(後続リクエスト)
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

メリット:

  • Last-Modified より正確
  • 時間に依存せず、コンテンツの hash を使用
  • 秒以下の変更も検出可能

Last-Modified vs ETag​

特性Last-ModifiedETag
精度秒単位コンテンツ hash、より正確
パフォーマンスより速いhash の計算が必要、より遅い
適用シーン一般的な静的リソース精密な制御が必要なリソース
優先度低高(ETag が優先)

3. How does browser caching work?​

ブラウザキャッシュの動作フローとは?

完全なキャッシュフロー​

┌──────────────────────────────────────────────┐
│ ブラウザのリソースリクエストフロー │
└──────────────────────────────────────────────┘
↓
1. Memory Cache を確認
↓
┌───────┴────────┐
│ キャッシュあり? │
└───────┬────────┘
Yes │ No
↓
2. Disk Cache を確認
↓
┌───────┴────────┐
│ キャッシュあり? │
└───────┬────────┘
Yes │ No
↓
3. Service Worker を確認
↓
┌───────┴────────┐
│ キャッシュあり? │
└───────┬────────┘
Yes │ No
↓
4. キャッシュの期限切れを確認
↓
┌───────┴────────┐
│ 期限切れ? │
└───────┬────────┘
Yes │ No
↓
5. ネゴシエーションキャッシュで検証
↓
┌───────┴────────┐
│ リソース変更? │
└───────┬────────┘
Yes │ No (304)
↓
6. サーバーに新リソースをリクエスト
↓
┌───────┴────────┐
│ 新リソースを返す │
│ (200 OK) │
└────────────────┘

実際の例​

// 初回リクエスト
GET /api/data.json
Response:
200 OK
Cache-Control: max-age=3600
ETag: "abc123"

{ data: "..." }

// ========== 1時間以内に再度リクエスト ==========
// 強キャッシュ:ローカルから直接読み取り、リクエストを送信しない
// Status: 200 OK (from disk cache)

// ========== 1時間後に再度リクエスト ==========
// ネゴシエーションキャッシュ:検証リクエストを送信
GET /api/data.json
If-None-Match: "abc123"

// リソース未変更
Response:
304 Not Modified
(body を返さず、ローカルキャッシュを使用)

// リソース変更済み
Response:
200 OK
ETag: "def456"

{ data: "new data" }

4. What are the common caching strategies?​

よく使われるキャッシュ戦略にはどのようなものがあるか?

1. 永続キャッシュ戦略(静的リソースに適用)​

// HTML:キャッシュしない、毎回チェック
Cache-Control: no-cache

// CSS/JS(hash 付き):永続キャッシュ
Cache-Control: public, max-age=31536000, immutable
// ファイル名:main.abc123.js

原理:

  • HTML はキャッシュしない、ユーザーが最新版を取得することを保証
  • CSS/JS は hash ファイル名を使用し、内容が変わるとファイル名も変わる
  • 旧バージョンは使用されず、新バージョンが再ダウンロードされる

2. 頻繁に更新されるリソースの戦略​

// API データ:短期間キャッシュ + ネゴシエーションキャッシュ
Cache-Control: max-age=60, must-revalidate
ETag: "abc123"

3. 画像リソースの戦略​

// ユーザーアバター:中期キャッシュ
Cache-Control: public, max-age=86400 // 1日

// ロゴ、アイコン:長期キャッシュ
Cache-Control: public, max-age=2592000 // 30日

// 動的画像:ネゴシエーションキャッシュ
Cache-Control: no-cache
ETag: "image-hash"

4. リソースタイプ別のキャッシュ推奨事項​

const cachingStrategies = {
// HTML ファイル
html: 'Cache-Control: no-cache',

// hash 付きの静的リソース
staticWithHash: 'Cache-Control: public, max-age=31536000, immutable',

// あまり更新されない静的リソース
staticAssets: 'Cache-Control: public, max-age=2592000',

// API データ
apiData: 'Cache-Control: private, max-age=60',

// ユーザー固有のデータ
userData: 'Cache-Control: private, no-cache',

// 機密データ
sensitive: 'Cache-Control: no-store',
};

5. Service Worker caching​

Service Worker キャッシュ

Service Worker は最も柔軟なキャッシュ制御を提供し、開発者がキャッシュロジックを完全にコントロールできます。

基本的な使い方​

// Service Worker の登録
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
}
// sw.js - Service Worker ファイル
const CACHE_NAME = 'my-app-v1';
const urlsToCache = [
'/',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png',
];

// インストールイベント:静的リソースをキャッシュ
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(urlsToCache);
})
);
});

// リクエストインターセプト:キャッシュ戦略を使用
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
// キャッシュ優先戦略
return response || fetch(event.request);
})
);
});

// 更新イベント:古いキャッシュをクリーンアップ
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
return caches.delete(cacheName);
}
})
);
})
);
});

よく使われるキャッシュ戦略​

1. Cache First(キャッシュ優先)​

// 適用:静的リソース
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request);
})
);
});

2. Network First(ネットワーク優先)​

// 適用:API リクエスト
self.addEventListener('fetch', (event) => {
event.respondWith(
fetch(event.request)
.then((response) => {
// キャッシュを更新
const responseClone = response.clone();
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone);
});
return response;
})
.catch(() => {
// ネットワーク失敗、キャッシュを使用
return caches.match(event.request);
})
);
});

3. Stale While Revalidate(期限切れ後に再検証)​

// 適用:高速なレスポンスが必要だが、更新も維持したいリソース
self.addEventListener('fetch', (event) => {
event.respondWith(
caches.match(event.request).then((cachedResponse) => {
const fetchPromise = fetch(event.request).then((networkResponse) => {
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, networkResponse.clone());
});
return networkResponse;
});

// キャッシュを返し、バックグラウンドで更新
return cachedResponse || fetchPromise;
})
);
});

6. How to implement cache busting?​

Cache Busting をどう実装するか?

Cache Busting は、ユーザーが最新のリソースを取得することを保証する技術です。

方法 1:ファイル名 Hash(推奨)​

// Webpack/Vite などのバンドルツールを使用
// 出力:main.abc123.js

// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash].js',
},
};
<!-- 参照を自動更新 -->
<script src="/js/main.abc123.js"></script>

メリット:

  • ✅ ファイル名が変わり、新しいファイルのダウンロードを強制
  • ✅ 旧バージョンはキャッシュのまま、無駄にならない
  • ✅ ベストプラクティス

方法 2:Query String バージョン番号​

<!-- バージョン番号を手動で更新 -->
<script src="/js/main.js?v=1.2.3"></script>
<link rel="stylesheet" href="/css/style.css?v=1.2.3" />

デメリット:

  • ❌ 一部の CDN は query string 付きのリソースをキャッシュしない
  • ❌ バージョン番号の手動管理が必要

方法 3:タイムスタンプ​

// 開発環境で使用
const timestamp = Date.now();
const script = document.createElement('script');
script.src = `/js/main.js?t=${timestamp}`;
document.body.appendChild(script);

用途:

  • 開発環境でキャッシュを回避
  • 本番環境には不向き(毎回新しいリクエストになる)

7. Common caching interview questions​

よくあるキャッシュの面接問題

問題 1:HTML をキャッシュさせないようにするには?​

クリックして回答を表示
Cache-Control: no-cache, no-store, must-revalidate
Pragma: no-cache
Expires: 0

または meta タグを使用:

<meta
http-equiv="Cache-Control"
content="no-cache, no-store, must-revalidate"
/>
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />

問題 2:なぜ Last-Modified だけでなく ETag を使うべきなのか?​

クリックして回答を表示

ETag の利点:

  1. より正確:秒以下の変更も検出可能
  2. コンテンツ駆動:時間ではなく、コンテンツの hash に基づく
  3. 時間の問題を回避:
    • ファイル内容は変わっていないが時間が変わった場合(再デプロイなど)
    • 周期的に同じ内容に戻るリソース
  4. 分散システム:異なるサーバー間で時間が同期していない可能性がある

例:

// ファイル内容は変わっていないが、Last-Modified が変わった
// 2024-01-01 12:00 - バージョン A をデプロイ(内容:abc)
// 2024-01-02 12:00 - バージョン A を再デプロイ(内容:abc)
// Last-Modified は変わったが、内容は同じ!

// ETag にはこの問題がない
ETag: 'hash-of-abc'; // 常に同じ

問題 3:from disk cache と from memory cache の違いは?​

クリックして回答を表示
特性Memory CacheDisk Cache
保存場所メモリ(RAM)ハードディスク
速度非常に高速より遅い
容量小(MB レベル)大(GB レベル)
持続性タブを閉じると削除永続的に保存
優先度高(優先的に使用)低

読み込みの優先順序:

1. Memory Cache(最速)
2. Service Worker Cache
3. Disk Cache
4. HTTP Cache
5. ネットワークリクエスト(最も遅い)

トリガー条件:

  • Memory Cache:直近にアクセスしたリソース(ページの再読み込みなど)
  • Disk Cache:しばらく前にアクセスしたリソース、またはサイズが大きいリソース

問題 4:ブラウザにリソースを強制的に再読み込みさせるには?​

クリックして回答を表示

開発段階:

// 1. Hard Reload(Ctrl/Cmd + Shift + R)
// 2. キャッシュをクリアして再読み込み

// 3. コードにタイムスタンプを追加
const script = document.createElement('script');
script.src = `/js/main.js?t=${Date.now()}`;

本番環境:

// 1. ファイル名 hash を使用(ベストプラクティス)
main.abc123.js // Webpack/Vite が自動生成

// 2. バージョン番号を更新
<script src="/js/main.js?v=2.0.0"></script>

// 3. Cache-Control を設定
Cache-Control: no-cache // 強制検証
Cache-Control: no-store // まったくキャッシュしない

問題 5:PWA オフラインキャッシュはどう実装するか?​

クリックして回答を表示
// sw.js - Service Worker
const CACHE_NAME = 'pwa-v1';
const OFFLINE_URL = '/offline.html';

// インストール時にオフラインページをキャッシュ
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll([
OFFLINE_URL,
'/styles/offline.css',
'/images/offline-icon.png',
]);
})
);
});

// リクエストインターセプト
self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate') {
event.respondWith(
fetch(event.request).catch(() => {
// ネットワーク失敗、オフラインページを表示
return caches.match(OFFLINE_URL);
})
);
}
});

完全な PWA キャッシュ戦略:

// 1. 静的リソースをキャッシュ
caches.addAll(['/css/', '/js/', '/images/']);

// 2. API リクエスト:Network First
// 3. 画像:Cache First
// 4. HTML:Network First、失敗時にオフラインページを表示

8. Best practices​

ベストプラクティス

✅ 推奨される方法​

// 1. HTML - キャッシュしない、ユーザーが最新版を取得することを保証
// Response Headers:
Cache-Control: no-cache

// 2. CSS/JS(hash 付き)- 永続キャッシュ
// ファイル名:main.abc123.js
Cache-Control: public, max-age=31536000, immutable

// 3. 画像 - 長期キャッシュ
Cache-Control: public, max-age=2592000 // 30日

// 4. API データ - 短期キャッシュ + ネゴシエーションキャッシュ
Cache-Control: private, max-age=60
ETag: "api-response-hash"

// 5. Service Worker を使用してオフラインサポートを実装

❌ 避けるべき方法​

// ❌ 悪い例:HTML に長期キャッシュを設定
Cache-Control: max-age=31536000 // ユーザーが古いバージョンを見る可能性がある

// ❌ 悪い例:Cache-Control の代わりに Expires を使用
Expires: Wed, 21 Oct 2025 07:28:00 GMT // HTTP/1.0、非推奨

// ❌ 悪い例:キャッシュをまったく設定しない
// キャッシュヘッダーがなく、ブラウザの動作が不定

// ❌ 悪い例:すべてのリソースに同じ戦略を使用
Cache-Control: max-age=3600 // リソースタイプに応じて調整すべき

キャッシュ戦略の決定木​

静的リソースか?
├─ はい → ファイル名に hash があるか?
│ ├─ はい → 永続キャッシュ(max-age=31536000, immutable)
│ └─ いいえ → 中長期キャッシュ(max-age=2592000)
└─ いいえ → HTML か?
├─ はい → キャッシュしない(no-cache)
└─ いいえ → API か?
├─ はい → 短期キャッシュ + ネゴシエーション(max-age=60, ETag)
└─ いいえ → 更新頻度に応じて決定

Reference​