Webサイト高速化の鍵!HTTPキャッシュ制御とETag(304 Not Modified)の仕組みを徹底解説

Webアプリケーションのパフォーマンスを最適化する上で、ネットワーク通信量の削減は避けて通れない課題です。特に、容量の大きい画像や頻繁にアクセスされるAPIのレスポンスを毎回フルデータで送信することは、サーバーリソースとユーザーの通信帯域を大きく浪費します。この問題をエレガントに解決する仕組みが、HTTPのCache-ControlとETagを組み合わせた「条件付きリクエスト」です。
本記事では、サーバー側でデータの変更有無をスマートに判定し、変更がない場合はコンテンツ本体を返さずに高速応答する「304 Not Modified」の仕組みについて、モダンなWeb開発で求められるベストプラクティスとともに詳しく解説します。
この記事でわかること
ETagと304 Not Modified(条件付きリクエスト)が連動する具体的な仕組み- 意図したキャッシュ挙動を実現するための
Cache-Controlの最適な設定値 - レガシーなヘッダー(
Expires、Pragma)やLast-Modifiedの現代における扱い - バックエンド(Node.js/Express)での実践的な304応答の実装コード例
304 Not Modifiedを返す「条件付きリクエスト」の仕組み
クライアントとサーバー間でコンテンツの重複転送を防ぐため、HTTPには「手元のデータが最新かどうか」を問い合わせる仕組みが備わっています。
サーバーはデータを返却する際、その内容を識別するための固有のハッシュ値などをETag(Entity Tag)ヘッダーとして付与します。クライアント(ブラウザ)はこのETagとコンテンツをセットでローカルにキャッシュします。
次回以降のアクセス時、ブラウザは「もし手元のETagと一致しなかったら新しいデータをください」という意味を持つIf-None-Matchヘッダーにその値を格納してリクエストを送ります。サーバー側で現在の最新データから計算したETagと、リクエストに含まれるETagが一致した場合、データに変更がないと判断してステータスコード304 Not Modifiedを返します。このとき、レスポンスのボディ(本体データ)は空っぽで返却されるため、通信量を劇的に削減できます。
リクエストに応じてキャッシュヘッダーを最適化する
毎回サーバーにデータの変更有無を確認させたい場合
ユーザーに常に最新のデータを届けつつ、変更がないときだけキャッシュを使わせたいケースでは、Cache-Control: no-cacheを指定するのが正解です。
no-cacheという名前から「キャッシュを禁止する」と誤解されがちですが、正確には「キャッシュは保持してよいが、利用する前に必ずサーバーへ最新かどうかの検証(バリデーション)を行わなければならない」という意味になります。これを設定することで、ブラウザは必ず毎回If-None-Matchヘッダーを伴ったリクエストをサーバーに送信するようになります。
/* サーバーからのレスポンス例 */
HTTP/1.1 200 OK
Cache-Control: no-cache
ETag: "v1-hash-xyz12345"
[ここにコンテンツの本体データ]
一定時間はサーバーへの問い合わせすら発生させたくない場合
一定期間は完全にデータの更新がないと割り切り、ネットワーク通信そのものを発生させたくない場合は、Cache-Control: max-age=秒数を使用します。
例えばmax-age=3600を設定すると、ブラウザは最初のアクセスから1時間(3600秒)の間、サーバーに問い合わせることなくローカルキャッシュをそのまま画面に表示します。指定した時間が経過(満了)した後の最初のアクセス時に、初めてサーバーへIf-None-Matchを用いた確認リクエストが飛び、変更がなければ再び304を返してキャッシュの寿命が更新されます。
/* 1時間、検証なしでのキャッシュを許可する */
Cache-Control: max-age=3600
ETag: "v1-hash-xyz12345"
304応答時に含めるべきヘッダー・不要なヘッダーの整理
304レスポンスでもETagとCache-Controlは付与する
サーバーが「変更なし」と判断して304応答を返すとき、コンテンツ本体(ボディ)を空にするだけでなく、ヘッダーに何を含めるべきかを正しく把握しておく必要があります。
HTTPの仕様(RFC 9110)において、304レスポンスには「仮に200 OKで新しいデータを返すとした場合に付与されるはずのキャッシュ制御ヘッダー(ETagやCache-Control)」を同様に含めなければならない(SHOULD)と定められています。ブラウザは304レスポンスに含まれるこれらのヘッダーを見て、保持しているキャッシュのメタデータを更新するため、省略せずに必ず返却するように実装してください。
Content-TypeやContent-Lengthは省略する
304 Not Modifiedを返す状況では、コンテンツの本体を返却しないため、データの種類を表すContent-Typeやデータのサイズを表すContent-Lengthといったヘッダーは不要であり、原則として「含めない」ことが推奨されます。
万が一これらが含まれていてもモダンなブラウザは安全に処理してくれますが、ボディが0バイトであるにもかかわらず古いサイズ情報などが残っていると、プロキシサーバーやCDNが「データが破損している」と誤認する原因になり得ます。不要なヘッダーは明示的に除外してレスポンスを組み立てましょう。
/* 理想的な304 Not Modifiedレスポンスの形 */
HTTP/1.1 304 Not Modified
Date: Mon, 22 Jun 2026 15:15:00 GMT
Cache-Control: no-cache
ETag: "v1-hash-xyz12345"
(※Content-TypeやContent-Lengthは含めず、ボディも空)
現代のWeb開発におけるレガシーヘッダーの扱い方
ExpiresとPragmaはなくてよい
古い技術書や古いシステムのソースコードでは、キャッシュ制御にExpiresやPragmaが使われているのを見かけますが、現代のWeb開発においてはこれらは「完全に不要」です。
Expiresは絶対時刻で期限を示すため、サーバーとクライアントの時計がズレていると正常に動作しない欠点があり、HTTP/1.1のCache-Control: max-ageに役目を譲りました。また、Pragma: no-cacheもHTTP/1.0時代のレガシーな指定です。現在の主要なブラウザやCDNはすべてHTTP/1.1以降を前提として動作しており、Cache-Controlが存在する場合はそちらを優先して解釈するため、これらレガシーヘッダーをわざわざ出力する必要はありません。
Last-Modifiedは無理に実装しなくてよい
ファイルの最終更新日時を示すLast-Modifiedヘッダーも、ETagがすでに実装されているのであれば必須ではありません。
Last-Modified(検証時はIf-Modified-Since)は秒単位の精度しか持たないため、1秒未満の高速なデータ更新を識別できないという弱点があります。HTTPの仕様上、ETag(If-None-Match)とLast-Modified(If-Modified-Since)が同時に送られてきた場合、サーバーはより精度の高いETagを優先して検証することになっています。動的APIなどにおいて、最終更新日時の算出に余計なDBクエリが必要になるくらいであれば、スッパリと省略してETag単体で運用する方がシンプルかつ高効率です。
| ヘッダー名 | 304レスポンスでの必要性 | 現代のWeb開発における扱い・注意点 |
|---|---|---|
ETag |
必須(推奨) | キャッシュの一意な識別子。ブラウザのキャッシュメタデータ更新に必要。 |
Cache-Control |
必須(推奨) | no-cacheなどを継続させ、次回の条件付きリクエストを担保するために必要。 |
Content-Type |
不要(除外) | 本体(ボディ)を返さないため不要。中途半端に含めると誤動作の原因に。 |
Content-Length |
不要(除外) | 同様に本体がないため不要。出力する場合は値を0にする。 |
Last-Modified |
任意 | ETagがあれば省略可能。静的ファイルなどで勝手に付与される場合はそのままでOK。 |
**Expires / Pragma** |
完全に不要 | 古いHTTP/1.0互換のためのヘッダー。Cache-Controlがあればモダンブラウザは無視する。 |
この状況で困ったら:自作APIで304を返す実装パターン
一般的なWebサーバー(NginxやApacheなど)やモダンなWebフレームワーク(Next.jsなど)は、静的ファイルの配信時にETagの生成と304の返却を自動で行ってくれます。しかし、独自のバックエンドAPIで動的なデータを扱う場合は、開発者が明示的に条件付きリクエストの判定ロジックを実装する必要があります。
以下は、Node.js(Express)を用いて、データのハッシュ値を計算し、ブラウザからのIf-None-Matchを検証して304を返す実践的なコード例です。
import express from 'express';
import crypto from 'crypto';
const app = express();
// 組み込みのETag自動生成機能をオフにする(手動制御を明確にするため)
app.set('etag', false);
app.get('/api/resource', (req, res) => {
// 1. レスポンスとなる動的データ(DBからの取得などを想定)
const data = {
id: 101,
name: "Premium Product",
updatedAt: "2026-06-22T15:00:00Z"
};
const bodyString = JSON.stringify(data);
// 2. データからETag(SHA-1ハッシュ値)を生成
const currentETag = `"${crypto.createHash('sha1').update(bodyString).digest('base64')}"`;
// 3. ブラウザから送られてきた If-None-Match ヘッダーを取得
const clientETag = req.headers['if-none-match'];
// 4. キャッシュの検証
if (clientETag === currentETag) {
// データに変更がないため、304 Not Modifiedを返す
// このとき、Cache-ControlとETagは含め、Content-Typeやボディは送らない
return res
.status(304)
.set({
'Cache-Control': 'no-cache',
'ETag': currentETag
})
.end(); // ボディを空にして送信を終了
}
// 5. データが変更されていた(または初回アクセス)の場合は200 OKでフルデータを返す
res.status(200)
.set({
'Cache-Control': 'no-cache',
'ETag': currentETag,
'Content-Type': 'application/json'
})
.send(bodyString);
});
app.listen(3000, () => console.log('Server running on port 3000'));
チェックリスト
- [ ]
Cache-Control: no-cacheの意味を正しく理解しているか(キャッシュの禁止ではなく、毎回サーバーへ確認させるという意味になっているか) - [ ] 304レスポンスの際に、
ETagとCache-Controlを削らずに返しているか - [ ] 304レスポンスの際に、
Content-Typeなどのボディ関連ヘッダーを適切に除外、あるいはボディを完全に空(0バイト)にしているか - [ ] 不要になったレガシーヘッダー(
Expires,Pragma)を惰性で出力し続けていないか
まとめ
HTTPキャッシュの最適化は、サーバーの転送量を削減し、ユーザーの体感速度を向上させるための最もコストパフォーマンスの高いアプローチの1つです。
ETagを用いた条件付きリクエストは、複雑に見えてその基本原則は極めてシンプルです。Cache-Control: no-cacheによって毎回検証を強制し、サーバー側でハッシュ値を比較して304 Not Modifiedを返す。この一連のクリーンな流れを掴むことで、無駄のない洗練されたWebアプリケーションの構築が可能になります。古くからある不正確なキャッシュ情報に惑わされず、仕様に基づいた最小限のヘッダー構成でモダンなキャッシュ戦略を組み立てていきましょう。
この記事が、システムのパフォーマンス改善と、HTTPキャッシュまわりの理解を深める一助となれば幸いです。ご覧いただきありがとうございました。







