gazou-compressor.jp

ETag vs Last-Modified 速習ガイド:ブラウザ再検証の最小実装

“304 が想定より返らない”“毎回 200 で帯域が増える” —— 条件付きリクエストの失敗は ETag / Last-Modified の設計ズレが原因であることが多いです。両者は似て見えて 目的粒度が違う 指標です。本稿は 5分で判断を固定 し、無駄な帯域とバックエンドCPUを削減する最小実装を提示します。

先に結論
内容差を正確に追跡したい: 内容ハッシュ ETag + Cache-Control。更新頻度低い純静的: Last-Modified 単独でも可。

要点(TL;DR)

1. 背景:なぜ失敗するか

ビルド/デプロイパイプラインで圧縮や minify が変動し、実質同一内容でもバイト列が揺れるケースがあります。強ETagは1バイト差でもミスマッチとして 200 を返し、再検証が増えます。計測すると “304 期待” の 60% 程度しか成立しない例も。結果CDNヒット率が下がり egress コストが増えます。

2. 役割の整理

項目ETagLast-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分)

  1. レスポンスが頻繁に書式揺れ → ETag(hash) 優先
  2. 純静的・差分希薄 → Last-Modified のみ (mtime)
  3. CDNで差分判定を厳密化したい → 両方送出
  4. 304 成立率が低い → 強ETag→弱へ / 正規化確認
  5. SWR後段が詰まる → max-age=0 + s-maxage/SWR再検証

5. 公開前チェック(8項目)

6. まとめ

“差分 = ハッシュ、時刻 = 補助” に割り切ると実装はシンプルになります。弱ETag + SWR + 監視ダッシュボードの3点セットを最初に整え、304率が落ちたら強ETagや生成ロジックの回帰調査へ段階的に進む形で十分です。

7. FAQ補足

gazou-compressor.jp 編集部

画像圧縮・変換・背景除去などの実践テクニックと、Webで“速く・軽く・崩さない”ためのノウハウを発信しています。

関連記事

トピック/更新日の近いコンテンツ