定義
フレームワークの内蔵画像最適化が使えない状況で、大きな元画像を画面に必要な小さいサイズ・軽いフォーマットにサーバーが代わりに変換して返したいときに作る中間サーバー(エンドポイント)だ。ブラウザは元画像の代わりに、このサーバーがリサイズ・変換した画像を受け取る。
正確に言えば、サーバー画像プロキシはクライアントが要求した表示サイズに合わせて upstream(元画像がある対象サーバー)画像を fetch → リサイズ・フォーマット変換(例:WebP、容量の小さいウェブ画像フォーマット)→ 応答する HTTP エンドポイントだ。このようにフォーマット・サイズを変えることをトランスコーディングという。CDN edge キャッシュ(ユーザーに近いサーバーノードに結果を保存して再利用)と関数インメモリ LRU(Least Recently Used、長く使っていない項目から捨てるメモリキャッシュ)を重ねて使い、miss(キャッシュになく元画像を新たに処理する場合)時にはストリーミング tee(一つのデータストリームを二つに分け、一方はすぐ応答、一方はキャッシュに保存)で TTFB(Time To First Byte、リクエスト後に最初のバイトが届くまでの時間)を減らす。
なぜ必要か
フレームワークの images.unoptimized: true 設定だと、<Image> が元の PNG/JPEG をそのまま返すことがある。カルーセルのサムネイルのように画面上で小さい画像は、サーバーで 336〜672px の WebP に変えるだけで転送バイトが数十〜数百倍減り、LCP(Largest Contentful Paint、画面で最も大きいコンテンツが描画される時点の指標)のボトルネックが「画像ダウンロード」から「レンダー」に移ることが多い。パフォーマンス A/B では、転送バイトが localhost の LCP よりも再現可能な headline(代表指標)になる。
動作原理
SSRF は Server-Side Request Forgery の略で、攻撃者が url パラメータに内部ネットワークのアドレスを入れ、サーバーに代わりにリクエストさせる脆弱性だ。allowlist(許可されたホストのリスト)でこれを防ぐ。
Client GET /api/image-proxy?url=...&w=672
→ SSRF allowlist 検証
→ cache key = origin + pathname + "|w" // presigned query 除去
→ LRU HIT → Buffer 応答
→ MISS:
upstream fetch (full signed URL)
sharp().resize({ width: w }).webp({ quality: 60 })
stream.tee():
branch A → Response body (即時)
branch B → buffer 収集 → LRU.set (成功時のみ)
→ Cache-Control: public, s-maxage=31536000, immutable
| 層 | 役割 |
|---|---|
CDN (s-maxage) | cold start 以降 durable、エッジ配信 |
| モジュール LRU | warm lambda での反復 hit、ephemeral |
クライアントでは loader の代わりに src をプロキシ URL に直接置き換える方が、unoptimized 環境では決定的だ。<link rel="preload"> の href も同じ URL でないと二重ダウンロードを防げない。
実務適用
// cache key — query strip for rotating signatures
function cacheKey(url: string, w: number) {
const u = new URL(url);
return `${u.origin}${u.pathname}|${w}`;
}
// Response headers
headers.set(
"Cache-Control",
"public, max-age=86400, s-maxage=31536000, immutable",
);
wは固定集合のみ許可(それ以外は 400) — DoS・キャッシュキー爆発を防止。- Node runtime + sharp(または同等)。
- MSW/E2E: mock URL がルートに到達できない場合、テスト環境でプロキシを迂回する分岐を設ける。
トレードオフ
| 選択 | 利得 | コスト |
|---|---|---|
| 自前プロキシ | unoptimized の迂回、フォーマット・品質の制御 | サーバー CPU・SSRF 表面 |
toBuffer() 後に応答 | 実装がシンプル | miss の TTFB 増加 |
| ストリーミング tee | 最初のバイトが速い | ストリーム途中失敗時のキャッシュスキップ処理が必要 |
| WebP q60 | 小さいバイト | 品質・アーティファクト — 用途別に調整 |
使ってはいけない場合
- すでに CDN・画像 CDN が最適化・キャッシュを提供しているのに、重複したプロキシだけを追加する。
- allowlist なしで任意 URL を fetch する。
よくある間違い
- preload は元画像、
img srcはプロキシ — preload が無効・二重リクエスト。 - キャッシュキーに presigned URL 全体を含める — 署名ローテーションごとに miss。
- localhost trace の LCP だけを headline にする — 実配備の CDN の利得はもっと大きい。
関連概念
- ssrf-url-fetch-proxy-guard — fetch 前のセキュリティ
- lqip-blur-placeholder-ssr — blur placeholder(LCP 指標とは別)
- resource-priority — preload・fetchpriority
- largest-contentful-paint — LCP ボトルネックの移行