画像配信のキャッシュ設計:Cache-Control / immutable / SWR / ETag(Next.js・CDN・NGINX)
画像のキャッシュ設計は CWV と運用コストを左右します。結論は「不変資産は“超”長期」「可変資産はSWR+ETag」の二本立て。 これに CDNの s-maxage を足せば、TTFBと帯域を確実に削減できます。
先に結論(運用ルール)
- 不変=ハッシュ名にして
Cache-Control: public, max-age=31536000, immutable
。 - 可変=アップロード等は
max-age=3600, stale-while-revalidate=86400
+ETag
。 - CDNがあるなら s-maxage を使い、オリジンを守る。
要点(TL;DR)
- キャッシュ戦略は資産の性質で分ける(不変/可変)。
- 長期キャッシュはハッシュ名が前提。更新=URLが変わる。
- 可変はSWR + 条件付きGET(ETag)で安全に最新化。
1. なぜ“二本立て”が最短か
すべてを短命にすると毎回ダウンロード、すべてを長命にすると更新が届きません。URLにハッシュを含められる資産は“内容=URL”になるため長期固定が可能。一方、ユーザーアップロードなど URLを固定したい資産はSWR + 条件付きGETで“速さと正しさ”を両立します。
2. 最短フロー(実務)
- 静的画像はビルドでハッシュ名へ(例:
hero.2d3af6a.avif
)。 - Next.js
headers()
で不変=1年+immutable、可変=SWR を付与。 - API Route/画像プロキシでは Cache-Control と ETag/Last-Modified を返す。
- CDN採用時は s-maxage を設定。無効化時はURLのクエリやパスでバスティング。
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. 応用とハマりどころ
- 優先度の設計は別軸。ヒーローだけ preload/fetchpriority を使い、乱用しない。
- 画像の描画幅に応じた配信は srcset/sizes。キャッシュが理想的でも、過大DLでは台無し。
- 予約サイズ(
width/height
oraspect-ratio
)で CLSゼロ を維持。
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 を見比べるのがおすすめです。