ありがとう、Cloudflare Image Resizing
READYFOR Advent Calendar 2025 17日目の記事です!
はじめに
Web フロントエンド開発において、永遠の課題とも言える「画像の最適化」。
特にアート作品や写真ギャラリーのように、
- 高画質で見せたい
- トリミング(Crop)は絶対 NG
という要件がある場合、パフォーマンスとの両立は一気に難易度が跳ね上がります。
今回は Vite + React + Cloudflare R2 という構成においてなんと DB に画像サイズのデータがないという絶望的な状況から、Core Web Vitals(LCP/CLS)を改善し、ヌルっと表示される "なんかちょっといい感じの画像コンポーネント" を作った話をしたいと思います!
Core Web Vitals ってなに
Google が提唱する「Web の健全性を示す指標」ですが、画像最適化において特に重要なのは以下の 2 つです。
- LCP (Largest Contentful Paint): 表示速度。メインコンテンツ(大きな画像など)がどれくらい速く表示されたか
- CLS (Cumulative Layout Shift): 視覚的安定性。読み込み中にレイアウトがガタッとズレないか
画像が大きくコンテンツを占めるサイトにおいて、「大きな画像を表示したい(LCP 悪化リスク)」 と 「縦横比がバラバラ(CLS 悪化リスク)」 は、まさにこの指標の天敵です。
DB にサイズ情報がない
今回のアプリケーションの要件は以下の通りでした。
- SPA (Vite + React) で構築し、Edge 環境でホスト。
- 画像は Cloudflare R2 にあり、オリジナルのアスペクト比で表示したい(Crop 禁止)。
- DB には画像 URL があるだけで、width/height の情報が保存されていない。 ← よくあるけど大変きびしい
通常、CLS を防ぐには CSS の aspect-ratio が定石ですが、これは 「ロード前に画像の比率を知っている」 ことが前提です。
サイズがわからない画像を width: 100%; height: auto; で表示すると、画像がロードされた瞬間に高さが 0px から 1200px に広がり、盛大なレイアウトシフト(CLS)が発生します。これは UX としても SEO としても致命的です。
既存のアプローチとその限界
この課題に対する一般的なフロントエンドのアプローチを検討しました。
A: とりあえず正方形のプレースホルダーを置く
→ 画像ロード後に本来の比率(例えば縦長)になると、結局ガタつき(Shift)が発生する。却下
B: 画像の onLoad イベントで高さを取得する
→ ロード完了まで高さが確定しないため、CLS は防げない。却下
C: BlurHash などを生成して DB に入れておく
→ これができれば苦労しない(今回は DB スキーマやバックエンド処理を変更できない制約)
救世主:Cloudflare Image Resizing
ここで登場するのが、Cloudflare Image Resizing です。
通常はリサイズやフォーマット変換(WebP/AVIF)に使いますが、画像の情報だけJSONで返してくれる最高機能がついているのです
format=json オプション
画像の URL に format=json を付けてリクエストすると、画像データの代わりに以下のようなメタデータを返してくれます。
// GET /cdn-cgi/image/format=json/my-art-image.jpg
{
"width": 1200,
"height": 1600,
"format": "jpeg",
"size": 102400
}
これを使えば、重い画像をダウンロードする前に、一瞬でサイズだけ把握することが可能なんですね!!!!
(そのかわり一回多くリクエストすることになるのでキャッシュ戦略はちゃんとしましょう)
今回作った "いい感じ画像コンポーネント" の戦略
この機能を活用し、以下の戦略をとる React コンポーネントを実装しました
-
メタデータ先行取得:
コンポーネントがマウントされたら、まずformat=jsonで軽量なリクエストを飛ばす -
枠の確保(CLS 撲滅):
JSON から得たwidth/heightを使い、aspect-ratioを設定したコンテナ(プレースホルダー)を描画する。この時点でブラウザは「ここに縦長の画像が来る」とわかるため、レイアウトが確定します -
本画像の読み込み:
枠の中に、srcset(レスポンシブ対応)やloading="lazy"を設定したimgタグをレンダリング -
UX 演出:
画像が読み込まれたら (onLoad)、CSS Transition でフワッとフェードインさせる
実装イメージ(概念コード)
// 簡略化したコードイメージです
export const OptimizedArtImage = ({ src }) => {
const { data: meta } = useSWR(`/cdn-cgi/image/format=json/${src}`, fetcher);
const [isLoaded, setIsLoaded] = useState(false);
if (!meta) return <Skeleton height="300px" />; // メタデータ取得中の仮表示
return (
<div
style={{
aspectRatio: `${meta.width} / ${meta.height}`,
backgroundColor: "#f3f4f6",
}}
>
<img
src={`/cdn-cgi/image/width=1200,format=auto/${src}`}
alt="Art"
loading="lazy"
style={{ opacity: isLoaded ? 1 : 0, transition: "opacity 0.5s" }}
onLoad={() => setIsLoaded(true)}
/>
</div>
);
};
結果
このコンポーネントを導入した結果、劇的な変化がありました。
CLS (Cumulative Layout Shift)
- Before: 0.25 (不良) - 画像が表示されるたびに記事がガタガタ動く。
- After: 0.023 (ほぼ完璧) - ロード前から枠が確保されているため、微動だにしない。
LCP (Largest Contentful Paint)
Cloudflare の自動フォーマット変換(WebP/AVIF)と適切なリサイズにより、転送量が大幅に削減。スマホでの体感速度がかなり改善されました!
UX (ユーザー体験)
ロード中は「グレーの枠(正しい比率)」が表示され、読み込み終わるとフワッと絵が現れる。画像が大きなサイトとして非常に洗練された挙動になりました。
おわりに
DB にサイズ情報がないから CLS は仕方ないかぁ・・・と諦めていましたが、エッジ(Cloudflare)の機能をフロントエンドとうまく組み合わせることで、ユーザー体験を損なうことなく解決できました。
今回自分が採用していた R2 + Image Resizing の組み合わせは、単なる画像配信サーバーとしてだけでなく、こうしたメタデータ APIとしても使える点が非常に強力です。
React での画像戦略で迷っている方は、ぜひこの「メタデータ先行取得パターン」を試してみてください!
