Webアプリケーションのパフォーマンス最適化において、キャッシュ戦略の理解は欠かせません。適切なWebキャッシュの設計により、ページ読み込み速度の向上、サーバー負荷の軽減、通信コストの削減が実現できます。

この記事では、ブラウザキャッシュ、CDNキャッシュ、サーバーサイドキャッシュの違いから、Cache-Controlヘッダーの各ディレクティブ、ETagによる条件付きリクエスト、そしてCache Busting(キャッシュ無効化)の実装手法まで、Webキャッシュの仕組みを体系的に解説します。

この記事で学べること

  • Webキャッシュの種類と各レイヤーの役割
  • Cache-Controlヘッダーの各ディレクティブの正確な意味
  • ETagとLast-Modifiedによる条件付きリクエストの仕組み
  • 実践的なキャッシュ戦略の設計パターン
  • Cache Bustingによるキャッシュ無効化の実装方法

前提知識と実行環境

この記事を理解するために必要な前提知識と実行環境は以下の通りです。

項目 内容
前提知識 HTTPリクエスト/レスポンスの基本、HTMLの基礎
確認環境 Chrome DevTools、Node.js v20以上
対象読者 パフォーマンス改善に取り組むフロントエンド/バックエンド開発者

Webキャッシュとは

Webキャッシュとは、Webリソース(HTML、CSS、JavaScript、画像など)を一時的に保存し、同じリソースへの再リクエスト時に保存済みのデータを再利用する仕組みです。キャッシュを活用することで、ネットワーク通信を削減し、レスポンス速度を大幅に向上させることができます。

キャッシュがない場合の問題点

キャッシュを使用しない場合、以下の問題が発生します。

  • 通信の無駄: 同じリソースを何度もサーバーから取得
  • レスポンス遅延: 毎回ネットワーク通信が発生
  • サーバー負荷増大: 同一リソースへのリクエストが集中
  • 通信コスト: データ転送量の増加

キャッシュによる解決

sequenceDiagram
    participant Browser as ブラウザ
    participant Cache as キャッシュ
    participant Server as オリジンサーバー
    
    Browser->>Cache: リソース要求
    alt キャッシュにヒット
        Cache-->>Browser: キャッシュ済みリソース(高速)
    else キャッシュにミス
        Cache->>Server: リソース要求
        Server-->>Cache: リソース
        Cache-->>Browser: リソース(+キャッシュ保存)
    end

Webキャッシュの種類と階層構造

Webキャッシュは複数のレイヤーで動作します。リクエストの流れに沿って、各キャッシュレイヤーを理解しましょう。

flowchart LR
    A[ブラウザ] --> B[ブラウザキャッシュ]
    B --> C[CDNキャッシュ]
    C --> D[リバースプロキシ<br>キャッシュ]
    D --> E[サーバーサイド<br>キャッシュ]
    E --> F[オリジンサーバー]
    
    style B fill:#e1f5fe
    style C fill:#fff3e0
    style D fill:#f3e5f5
    style E fill:#e8f5e9

ブラウザキャッシュ(プライベートキャッシュ)

ブラウザキャッシュは、ユーザーのブラウザに保存されるキャッシュです。個人のデバイスに保存されるため「プライベートキャッシュ」とも呼ばれます。

特徴 説明
保存場所 ユーザーのローカルディスク
共有範囲 同一ユーザーの同一ブラウザのみ
制御方法 Cache-Controlヘッダー(private)
用途 個人向けリソース、認証済みコンテンツ

CDNキャッシュ(共有キャッシュ)

CDN(Content Delivery Network)は、世界中に分散配置されたエッジサーバーでコンテンツをキャッシュし、ユーザーに最も近い場所からリソースを配信します。

特徴 説明
保存場所 CDNエッジサーバー(地理的に分散)
共有範囲 複数ユーザー間で共有
制御方法 Cache-Controlヘッダー(public, s-maxage)
用途 静的アセット、公開コンテンツ

リバースプロキシキャッシュ

リバースプロキシ(Nginx、Varnishなど)は、オリジンサーバーの手前でリクエストを受け、キャッシュ済みのレスポンスを返すことでサーバー負荷を軽減します。

サーバーサイドキャッシュ

アプリケーションサーバー内部で、データベースクエリ結果やAPIレスポンスをメモリ(Redis、Memcached)にキャッシュします。

キャッシュタイプ 保存場所 主な用途
ブラウザキャッシュ クライアント 静的アセット、個人データ
CDNキャッシュ エッジサーバー グローバル配信、静的コンテンツ
リバースプロキシ プロキシサーバー 動的コンテンツのキャッシュ
サーバーサイド アプリケーション/メモリ DB結果、API応答

Cache-Controlヘッダーの完全解説

Cache-Controlは、HTTPキャッシュの動作を制御する最も重要なヘッダーです。サーバーからのレスポンスに含めることで、キャッシュの保存可否、有効期間、再検証の要否などを指定できます。

基本構文

1
Cache-Control: ディレクティブ1, ディレクティブ2, ...

主要なディレクティブ一覧

ディレクティブ 説明
public 共有キャッシュ(CDN等)に保存可能
private ブラウザキャッシュのみ保存可能
no-cache キャッシュ保存は許可するが、使用前に必ず再検証が必要
no-store キャッシュへの保存を完全に禁止
max-age=秒数 キャッシュの有効期間を秒単位で指定
s-maxage=秒数 共有キャッシュ(CDN等)専用の有効期間
must-revalidate 有効期限切れ後、必ずオリジンで再検証
immutable リソースが変更されないことを示す

no-cacheとno-storeの違い(重要)

Cache-Controlで最も混同されやすいのがno-cacheno-storeです。両者の違いを正確に理解しましょう。

flowchart TD
    A[リクエスト発生] --> B{Cache-Controlは?}
    B -->|no-store| C[キャッシュ保存禁止<br>毎回サーバーに問い合わせ]
    B -->|no-cache| D[キャッシュに保存]
    D --> E{使用時に再検証}
    E -->|304 Not Modified| F[キャッシュを使用]
    E -->|200 OK| G[新しいリソースを取得]
    
    style C fill:#ffcdd2
    style D fill:#c8e6c9

no-cache: キャッシュへの保存は許可しますが、キャッシュを使用する前に必ずオリジンサーバーへ条件付きリクエストを送信して再検証が必要です。リソースが変更されていなければ304レスポンスでキャッシュを再利用できます。

no-store: キャッシュへの保存自体を禁止します。機密情報など、絶対にキャッシュしてはいけないリソースに使用します。

1
2
3
4
5
6
7
8
# no-cache: キャッシュOK、使用時に検証必須
Cache-Control: no-cache

# no-store: キャッシュ禁止(機密データ向け)
Cache-Control: no-store

# 組み合わせ例(最も厳格)
Cache-Control: no-store, no-cache, must-revalidate

max-ageとs-maxageの使い分け

max-ageはすべてのキャッシュに適用される有効期間、s-maxageは共有キャッシュ(CDN等)専用の有効期間です。

1
2
# ブラウザ: 1時間、CDN: 1日キャッシュ
Cache-Control: public, max-age=3600, s-maxage=86400

この設定では、ブラウザは1時間キャッシュを保持し、CDNは1日間キャッシュを保持します。CDNでは長くキャッシュしつつ、ブラウザでは短いサイクルで更新を確認できます。

must-revalidateの役割

must-revalidateは、キャッシュの有効期限が切れた後、必ずオリジンサーバーで再検証することを強制します。オリジンサーバーに到達できない場合、504 Gateway Timeoutを返します。

1
2
# 1時間有効、期限切れ後は必ず再検証
Cache-Control: max-age=3600, must-revalidate

immutableによる最適化

immutableは、リソースが決して変更されないことをブラウザに伝えます。ページリロード時でも再検証リクエストを送信しないため、パフォーマンスが向上します。

1
2
# ハッシュ付きファイル名のアセットに最適
Cache-Control: public, max-age=31536000, immutable

条件付きリクエスト(ETagとLast-Modified)

Cache-Controlでキャッシュの有効期限を設定しても、期限切れ後やno-cache指定時には再検証が必要です。この再検証を効率的に行うのが「条件付きリクエスト」です。

条件付きリクエストの流れ

sequenceDiagram
    participant Browser as ブラウザ
    participant Server as サーバー
    
    Note over Browser,Server: 初回リクエスト
    Browser->>Server: GET /image.png
    Server-->>Browser: 200 OK<br>ETag: "abc123"<br>Last-Modified: Wed, 01 Jan 2025 00:00:00 GMT
    
    Note over Browser,Server: キャッシュ検証(リソース未変更)
    Browser->>Server: GET /image.png<br>If-None-Match: "abc123"<br>If-Modified-Since: Wed, 01 Jan 2025 00:00:00 GMT
    Server-->>Browser: 304 Not Modified(ボディなし)
    
    Note over Browser,Server: キャッシュ検証(リソース変更済み)
    Browser->>Server: GET /image.png<br>If-None-Match: "abc123"
    Server-->>Browser: 200 OK<br>ETag: "def456"<br>(新しいリソース)

ETagとIf-None-Match

ETag(Entity Tag)は、リソースの特定バージョンを識別する一意の識別子です。ファイルのハッシュ値やバージョン番号が使用されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# サーバーレスポンス(初回)
HTTP/1.1 200 OK
Content-Type: image/png
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Cache-Control: no-cache

# クライアントリクエスト(再検証時)
GET /image.png HTTP/1.1
Host: example.com
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

# サーバーレスポンス(未変更の場合)
HTTP/1.1 304 Not Modified
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"

Last-ModifiedとIf-Modified-Since

Last-Modifiedは、リソースの最終更新日時を示すヘッダーです。ETagより精度は低いですが、実装が簡単です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# サーバーレスポンス(初回)
HTTP/1.1 200 OK
Content-Type: text/html
Last-Modified: Tue, 07 Jan 2025 10:00:00 GMT

# クライアントリクエスト(再検証時)
GET /page.html HTTP/1.1
If-Modified-Since: Tue, 07 Jan 2025 10:00:00 GMT

# サーバーレスポンス(未変更の場合)
HTTP/1.1 304 Not Modified

ETagとLast-Modifiedの比較

観点 ETag Last-Modified
精度 バイト単位で正確 秒単位(1秒未満の変更を検知不可)
生成コスト ハッシュ計算が必要 ファイルシステムから取得可能
適用場面 厳密な一致が必要な場合 通常の静的ファイル
優先度 If-None-Matchが優先 If-None-Matchがない場合に使用

リソース種別ごとのキャッシュ戦略

効果的なキャッシュ戦略は、リソースの特性に応じて使い分けることが重要です。

静的アセット(JS/CSS/画像)のキャッシュ戦略

ビルド時にハッシュ付きファイル名を生成する静的アセットには、長期間のキャッシュを設定します。

1
2
# ハッシュ付きファイル(例: app.a1b2c3d4.js)
Cache-Control: public, max-age=31536000, immutable

設定のポイントは以下の通りです。

  • max-age=31536000: 1年間キャッシュ
  • immutable: リロード時も再検証しない
  • public: CDNでもキャッシュ可能

HTMLドキュメントのキャッシュ戦略

HTMLはアプリケーションのエントリーポイントであり、常に最新版を取得する必要があります。

1
2
3
# HTMLファイル
Cache-Control: no-cache
ETag: "html-version-hash"

no-cacheを指定することで、毎回ETagによる検証を行い、更新があれば即座に反映されます。

APIレスポンスのキャッシュ戦略

APIレスポンスは、データの性質に応じて戦略を変えます。

1
2
3
4
5
6
7
8
# 公開データ(変更頻度低)
Cache-Control: public, max-age=300, s-maxage=600

# ユーザー固有データ
Cache-Control: private, max-age=60

# リアルタイムデータ
Cache-Control: no-store

推奨キャッシュ設定まとめ

リソースタイプ Cache-Control設定 理由
ハッシュ付き静的アセット public, max-age=31536000, immutable ファイル名変更で無効化
バージョンなし静的アセット public, max-age=86400 1日でリフレッシュ
HTML no-cache 常に最新を検証
認証済みAPI private, no-cache ユーザー固有、要検証
機密データ no-store キャッシュ禁止

キャッシュ無効化(Cache Busting)の実装

長期間のキャッシュを設定した場合、リソース更新時にクライアントのキャッシュを無効化する仕組みが必要です。これを「Cache Busting」と呼びます。

ファイル名にハッシュを付与する方式

最も一般的で信頼性の高い方法です。ビルドツール(Webpack、Vite等)が自動的にファイル内容のハッシュをファイル名に付与します。

1
2
3
<!-- ハッシュ付きファイル名 -->
<link rel="stylesheet" href="/assets/styles.a1b2c3d4.css">
<script src="/assets/app.e5f6g7h8.js"></script>

Viteの設定例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        // ファイル名にハッシュを含める
        entryFileNames: 'assets/[name].[hash].js',
        chunkFileNames: 'assets/[name].[hash].js',
        assetFileNames: 'assets/[name].[hash].[ext]'
      }
    }
  }
});

クエリ文字列による方式

URLのクエリパラメータにバージョン番号やタイムスタンプを付与する方法です。

1
2
3
<!-- クエリ文字列方式 -->
<link rel="stylesheet" href="/styles.css?v=1.2.3">
<script src="/app.js?t=1704607200"></script>

注意点として、一部のプロキシやCDNはクエリ文字列付きURLをキャッシュしない設定になっている場合があります。

Service Workerによるキャッシュ制御

Service Workerを使用すると、より細かいキャッシュ制御が可能です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// sw.js
const CACHE_NAME = 'app-cache-v2';
const urlsToCache = [
  '/',
  '/styles.css',
  '/app.js'
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then((cache) => cache.addAll(urlsToCache))
  );
});

self.addEventListener('activate', (event) => {
  // 古いキャッシュを削除
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames.map((cacheName) => {
          if (cacheName !== CACHE_NAME) {
            return caches.delete(cacheName);
          }
        })
      );
    })
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => response || fetch(event.request))
  );
});

サーバー側でのキャッシュヘッダー実装

各サーバー環境でのCache-Controlヘッダー設定方法を紹介します。

Nginxでの設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# nginx.conf

# 静的アセット(ハッシュ付き)
location ~* \.[a-f0-9]{8}\.(js|css|png|jpg|gif|svg|woff2?)$ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
}

# 通常の静的ファイル
location ~* \.(js|css|png|jpg|gif|svg|woff2?)$ {
    expires 1d;
    add_header Cache-Control "public, max-age=86400";
}

# HTMLファイル
location ~* \.html$ {
    add_header Cache-Control "no-cache";
    etag on;
}

# APIレスポンス
location /api/ {
    add_header Cache-Control "private, no-cache";
}

Expressでの設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// server.js
import express from 'express';
import { createHash } from 'crypto';
import { readFileSync, statSync } from 'fs';

const app = express();

// 静的ファイルのキャッシュ設定
app.use('/assets', express.static('public/assets', {
  maxAge: '1y',
  immutable: true,
  setHeaders: (res, path) => {
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
  }
}));

// HTMLファイル
app.get('/', (req, res) => {
  const html = readFileSync('./public/index.html');
  const etag = createHash('md5').update(html).digest('hex');
  
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('ETag', `"${etag}"`);
  
  // ETag検証
  if (req.headers['if-none-match'] === `"${etag}"`) {
    return res.status(304).end();
  }
  
  res.type('html').send(html);
});

// APIエンドポイント
app.get('/api/data', (req, res) => {
  res.setHeader('Cache-Control', 'private, max-age=60');
  res.json({ data: 'example' });
});

app.listen(3000);

CDN(Cloudflare)でのキャッシュ設定

Cloudflareでは、Page RulesまたはCache Rulesでパス別にキャッシュ動作を設定できます。

パスパターン キャッシュレベル Edge TTL Browser TTL
*.example.com/assets/* Cache Everything 1年 1年
*.example.com/*.html Standard なし なし
*.example.com/api/* Bypass - -

Chrome DevToolsでのキャッシュ検証

開発中にキャッシュ動作を確認する方法を解説します。

Networkパネルでのヘッダー確認

Chrome DevToolsのNetworkタブで、各リソースのリクエスト/レスポンスヘッダーを確認できます。

確認すべきポイントは以下の通りです。

  1. Response Headers

    • Cache-Control: キャッシュディレクティブ
    • ETag: リソースの識別子
    • Last-Modified: 最終更新日時
  2. Status Code

    • 200 OK: 新規取得
    • 304 Not Modified: キャッシュ再利用
    • 200 OK (from disk cache): ローカルキャッシュから取得
    • 200 OK (from memory cache): メモリキャッシュから取得

キャッシュの無効化テスト

DevToolsでキャッシュを無効化してテストする方法です。

  1. DevToolsを開く(F12)
  2. Networkタブを選択
  3. 「Disable cache」にチェックを入れる
  4. ページをリロード

この状態では、毎回サーバーからリソースを取得するため、キャッシュなしの動作を確認できます。

Application > Storageでのキャッシュクリア

DevToolsのApplicationタブ > Storageセクションで、特定のキャッシュをクリアできます。

  • Cache Storage(Service Worker)
  • Application Cache(非推奨)

キャッシュ戦略のベストプラクティス

推奨パターン

flowchart TD
    A[リソースの種類を判定] --> B{変更されるか?}
    B -->|頻繁に変更| C{機密データか?}
    B -->|ほぼ不変| D[長期キャッシュ<br>max-age=1年]
    C -->|はい| E[no-store]
    C -->|いいえ| F{ユーザー固有か?}
    F -->|はい| G[private, no-cache]
    F -->|いいえ| H[public, no-cache]
    
    D --> I[Cache Busting必須<br>ハッシュ付きファイル名]
    
    style D fill:#c8e6c9
    style E fill:#ffcdd2
    style G fill:#fff3e0
    style H fill:#e1f5fe

アンチパターン

避けるべきキャッシュ設定のパターンです。

1. max-ageなしでimmutableを使用

1
2
# NG: max-ageがないとimmutableの意味がない
Cache-Control: immutable

2. 動的コンテンツにmax-ageだけを設定

1
2
# NG: must-revalidateがないと古いキャッシュが使われる可能性
Cache-Control: max-age=3600

3. publicと機密データの組み合わせ

1
2
3
# NG: 認証済みデータがCDNにキャッシュされる危険
Cache-Control: public, max-age=3600
Authorization: Bearer token...

推奨される設定パターン

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 静的アセット(ハッシュ付き)
Cache-Control: public, max-age=31536000, immutable

# HTML/動的ページ
Cache-Control: no-cache
ETag: "page-hash"

# 認証済みAPIレスポンス
Cache-Control: private, no-cache, no-store

# 公開APIレスポンス(短期キャッシュ)
Cache-Control: public, max-age=300, s-maxage=600, must-revalidate

まとめ

Webキャッシュの適切な設計は、パフォーマンス最適化の要です。この記事で解説した内容を振り返ります。

  • キャッシュの階層: ブラウザキャッシュ、CDNキャッシュ、サーバーサイドキャッシュの役割を理解する
  • Cache-Controlディレクティブ: no-cacheno-storeの違い、max-ages-maxageの使い分けを正確に把握する
  • 条件付きリクエスト: ETagとLast-Modifiedによる効率的な再検証で帯域を節約する
  • Cache Busting: ハッシュ付きファイル名による確実なキャッシュ無効化を実装する
  • リソース別戦略: 静的アセット、HTML、APIそれぞれに適した設定を適用する

キャッシュ戦略は一度設定して終わりではなく、アプリケーションの特性や要件の変化に応じて継続的に見直すことが重要です。

参考リンク