gazou-compressor.jp

画像配信のキャッシュ設計:Cache-Control / immutable / SWR / ETag(Next.js・CDN・NGINX)

画像のキャッシュ設計は CWV と運用コストを左右します。結論は「不変資産は“超”長期」「可変資産はSWR+ETag」の二本立て。 これに CDNの s-maxage を足せば、TTFBと帯域を確実に削減できます。

画像の配信自体は srcset/sizes <picture>の切替 と組み合わせ、 表示の安定は CLSゼロ設計 を参照。
先に結論(運用ルール)
  • 不変=ハッシュ名にして Cache-Control: public, max-age=31536000, immutable
  • 可変=アップロード等 max-age=3600, stale-while-revalidate=86400 + ETag
  • CDNがあるなら s-maxage を使い、オリジンを守る

要点(TL;DR)

1. なぜ“二本立て”が最短か

すべてを短命にすると毎回ダウンロード、すべてを長命にすると更新が届きません。URLにハッシュを含められる資産は“内容=URL”になるため長期固定が可能。一方、ユーザーアップロードなど URLを固定したい資産はSWR + 条件付きGETで“速さと正しさ”を両立します。

2. 最短フロー(実務)

  1. 静的画像はビルドでハッシュ名へ(例: hero.2d3af6a.avif)。
  2. Next.js headers()不変=1年+immutable可変=SWR を付与。
  3. API Route/画像プロキシでは Cache-ControlETag/Last-Modified を返す。
  4. CDN採用時は s-maxage を設定。無効化時はURLのクエリやパスでバスティング。
  5. curl -I でヘッダを確認し、自動テストに組み込む。

3. 実装レシピ(コピペOK)

3.1 Next.js: headers() で付与

// JavaScript// 1) Next.js 15: next.config.jsで静的画像に長期キャッシュ
/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [
      // public 配下の拡張子(=ビルドでハッシュ付きにしておくこと)
      {
        source: "/:path*\.(?:avif|webp|jpe?g|png|gif|svg)$",
        headers: [
          { key: "Cache-Control", value: "public, max-age=31536000, immutable" },
        ],
      },
      // 可変アップロード(例:/uploads/** は1時間+SWR)
      {
        source: "/uploads/:path*",
        headers: [
          { key: "Cache-Control", value: "public, max-age=3600, stale-while-revalidate=86400" },
        ],
      },
    ];
  },
};
module.exports = nextConfig;

3.2 API Route(CDN向け s-maxage + ETag)

// TypeScript// 2) API Routeで画像を返すときのキャッシュ(s-maxageでCDNにも効かせる)
// app/api/image/[...path]/route.ts
import { NextRequest } from "next/server";

export async function GET(req: NextRequest, { params }: { params: { path: string[] } }) {
  const key = params.path.join("/"); // 例: key = "avatars/u123.webp"
  // 実際はS3/Blob等から取得
  const origin = await fetch("https://img.example.com/" + key, { cache: "no-store" });

  if (!origin.ok) return new Response("Not found", { status: 404 });

  // 元のヘッダを継承しつつ、キャッシュ方針を上書き
  const headers = new Headers(origin.headers);
  headers.set(
    "Cache-Control",
    // ブラウザ: 1分, CDN: 1日, 長時間のSWR: 7日
    "public, max-age=60, s-maxage=86400, stale-while-revalidate=604800"
  );
  // ETag/Last-Modified を残す(条件付きリクエストで304が返せる)
  const etag = headers.get("ETag");
  if (!etag) headers.set("ETag", `W/"${Date.now()}-${key}" `); // 無ければ弱ETagを付与(簡易)

  const buf = await origin.arrayBuffer();
  return new Response(buf, { status: 200, headers });
}

3.3 NGINX の例

// NGINX# 3) NGINX: 静的(ハッシュ名)は1年+immutable、可変はSWR+ETag
# /etc/nginx/conf.d/site.conf の一例
map $sent_http_etag $cache_control_uploads {
  default "public, max-age=3600, stale-while-revalidate=86400";
}

# 不変資産(/img/** にハッシュ付きで配置)
location ~* \.(?:avif|webp|jpe?g|png|gif|svg)$ {
  add_header Cache-Control "public, max-age=31536000, immutable";
  try_files $uri =404;
}

# 可変アップロード(ETagをon)
location ^~ /uploads/ {
  etag on;
  add_header Cache-Control $cache_control_uploads;
  try_files $uri =404;
}

3.4 ハッシュ名運用(フィンガープリント)

// Notes# 4) ファイル名にハッシュ(fingerprint)を付けて“安全に長期キャッシュ”
# 例: hero-1600.avif -> hero-1600.2d3af6a.avif
# 生成方法はビルドパイプラインに依存(例:Sharpやローダで contenthash を付与)
# 参照するHTML/JS側は新ハッシュへ書き換わるので、古いキャッシュは自然に切替

3.5 curl で検証

// Shell# 5) 検証(ヘッダの確認)
curl -I https://gazou-compressor.jp/img/hero-1600.2d3af6a.avif
curl -I https://gazou-compressor.jp/uploads/user/banner.webp

# 代表的な見え方
# 静的(不変)
# Cache-Control: public, max-age=31536000, immutable
# 可変
# Cache-Control: public, max-age=3600, stale-while-revalidate=86400
# ETag: "686897696a7c876b7e"

4. 応用とハマりどころ

5. 公開前チェック

// Checklist# 公開前チェック(画像キャッシュ)
- 資産を分類:不変(ビルド生成・ハッシュ名) / 可変(アップロード)
- 不変: public, max-age=31536000, immutable
- 可変: public, max-age=3600, stale-while-revalidate=86400 + ETag/Last-Modified
- CDNがある: s-maxage を設定し、オリジンTTFBを下げる
- URLはハッシュでキャッシュバスティング。参照の差し替えを自動化
- 画像は width/height(または aspect-ratio)で CLSゼロ(関連: /articles/cls-zero-images-ads)
- 優先度(preload/fetchpriority)を乱用しない(関連: /articles/priority-hints-preload-fetchpriority)

6. まとめ

画像配信はキャッシュ設計を定型化できれば勝ちです。 「不変=ハッシュ+1年immutable」「可変=SWR+ETag」に分け、CDNの s-maxage を適用。 あとは /compare で劣化を、 /compressor で容量を確認して仕上げましょう。

FAQ(よくある質問)

画像形式の基本方針は?(写真/スクショ/透過)
写真は AVIF / WebP(画質80–85%目安)、UIやスクショはPNG / WebP Lossless、単色ロゴはSVGが基本です。 実装の詳細は srcset/sizes設計ガイド スクショ最適化 を参照してください。
圧縮しても画質を落とさないコツは?
実表示幅に合わせたリサイズ → 過大ダウンロードを防ぎ、srcset/sizes を 実描画幅に一致させます。画質は写真で 80–85% を起点に、ノイズやエッジを目視確認。 仕上げは /compare で Before/After を見比べるのがおすすめです。
CLSを悪化させない画像の置き方は?
すべての画像に width/height(または親に aspect-ratio)を与え、広告・埋め込みは 予約サイズを先に確保します。詳しくは CLSゼロ設計ガイド を参照。

公開:2025-08-31

gazou-compressor.jp 編集部

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

関連記事