ETag vs Last-Modified 速習ガイド:ブラウザ再検証の最小実装
“304 が想定より返らない”“毎回 200 で帯域が増える” —— 条件付きリクエストの失敗は ETag / Last-Modified の設計ズレが原因であることが多いです。両者は似て見えて 目的粒度が違う 指標です。本稿は 5分で判断を固定 し、無駄な帯域とバックエンドCPUを削減する最小実装を提示します。
先に結論
内容差を正確に追跡したい: 内容ハッシュ ETag + Cache-Control。更新頻度低い純静的: Last-Modified 単独でも可。
要点(TL;DR)
- ETag=内容指紋 / Last-Modified=最終更新時刻近似。
- 軽微再エンコードでバイト差 → 強ETagだと304逃す → 弱ETag (W/) で回避。
- 動的APIは JSON 正規化→hash で ETag を決定、時刻は補助。
- SWR 戦略と併用: 期限内ヒット + 期限後再検証304。
1. 背景:なぜ失敗するか
ビルド/デプロイパイプラインで圧縮や minify が変動し、実質同一内容でもバイト列が揺れるケースがあります。強ETagは1バイト差でもミスマッチとして 200 を返し、再検証が増えます。計測すると “304 期待” の 60% 程度しか成立しない例も。結果CDNヒット率が下がり egress コストが増えます。
- HTML: 空白/日付埋め込み差分
- 画像: 可逆再最適化(PNG最適化)で CRC 変動
- JSON API: key順序/float丸めの揺れ
2. 役割の整理
項目 | ETag | Last-Modified |
---|---|---|
主目的 | 内容同一性 | 更新時刻近似 |
粒度 | バイト/正規化後ハッシュ | 秒精度 |
精度低下要因 | 強ETag=再エンコード揺れ | NFS遅延/ミラー差 |
推奨用途 | API/頻繁差分 | 静的生成物 |
弱点 | 生成コスト | 同内容同時刻区別不可 |
3. 最小実装(Next.js Route Handler)
// app/api/stats/route.ts import crypto from 'node:crypto'; import { NextResponse } from 'next/server'; export async function GET() { const data = await getStats(); // オブジェクト const normalized = JSON.stringify(data); // key順序固定 const hash = crypto.createHash('sha1').update(normalized).digest('base64').slice(0, 16); const etag = 'W/"'+hash+'"'; const ims = headers().get('if-modified-since'); const inm = headers().get('if-none-match'); if (inm === etag) { return new NextResponse(null, { status: 304, headers: { 'ETag': etag } }); } return NextResponse.json(data, { headers: { 'ETag': etag, 'Cache-Control': 'public, max-age=0, s-maxage=300, stale-while-revalidate=600', 'Last-Modified': new Date().toUTCString(), } }); }
ポイント: key順序固定
/ 弱ETag
/ SWR による再検証分離。
4. 判断フロー(5分)
- レスポンスが頻繁に書式揺れ → ETag(hash) 優先
- 純静的・差分希薄 → Last-Modified のみ (mtime)
- CDNで差分判定を厳密化したい → 両方送出
- 304 成立率が低い → 強ETag→弱へ / 正規化確認
- SWR後段が詰まる → max-age=0 + s-maxage/SWR再検証
5. 公開前チェック(8項目)
- 304 成立率 (→ 監視) が期待値 > 85%
- ETag 正規化ハッシュが環境差で揺れない
- Last-Modified 時刻が未来にならない
- SWR 期限後は 304 or 差分 200 に収束
- 強ETagが不要に304阻害していない
- 帯域計測でベースライン比削減を説明可能
- ヘッダテスト (curl -I) で存在確認
- ドキュメント化(運用手順/更新時トリガ)
6. まとめ
“差分 = ハッシュ、時刻 = 補助” に割り切ると実装はシンプルになります。弱ETag + SWR + 監視ダッシュボードの3点セットを最初に整え、304率が落ちたら強ETagや生成ロジックの回帰調査へ段階的に進む形で十分です。
7. FAQ補足
- Q. CDNが勝手に ETag を書き換える? → 一部プロキシはgzip差異で付け直す。Vary: Accept-Encoding を明示。
- Q. JSON差分パッチ応答は? → まず304率最適化後。早 premature complexity を避ける。