株式会社ファストコーディングのフルスタックエンジニア MrFireです。
先日、久しぶりにバイクで房総半島をツーリングしてきました。途中で見つけた海沿いのカフェが気になって、スマホでサイトを開いたのですが、、、、メイン画像がなかなか表示されない。3G回線でもないのに、画面の半分が白いまま数秒間待たされました。結局その場で「まあいいか」と閉じてしまったのですが、帰宅後にDevToolsで見てみると、案の定トップの画像が3MBのPNGでした。
ツーリング先でもDevToolsを開いてしまうのは職業病だと思いたい。
この体験から数日後、ちょうど案件でECサイトのパフォーマンス改善を依頼されました。Core Web VitalsのLCPが4.8秒で真っ赤。原因の大半が画像まわりだったので、今回はAI駆動開発のアプローチでNext.jsの画像最適化を徹底した話を書きます。
案件の状況:LCPが4.8秒で真っ赤だった
クライアントは30商品ほどを扱うアパレルECサイトを運営していました。Next.js 14のApp Routerで構築されたサイトですが、PageSpeed Insightsのスコアはモバイルで38点。LCPは4.8秒、CLSは0.15と、Core Web Vitalsの指標が軒並み基準を下回っていました。
「商品画像を高画質で見せたい」というクライアントの意向から、撮影した画像をそのままアップロードしていたのが根本原因です。1枚あたり2〜5MBのPNGファイルが、トップページだけで8枚並んでいました。
AIに画像実装のレビューを依頼する
まず、既存のコードをClaudeに渡して問題点を洗い出してもらいました。プロンプトは以下のような内容です。
「このNext.jsプロジェクトの画像実装をレビューしてください。パフォーマンス観点で問題がある箇所と、next/imageを使った改善案を提示してください。」
AIが指摘したポイントは、大きく3つありました。
- HTMLの
<img>タグを直接使っており、Next.jsの画像最適化パイプラインが機能していない widthとheightの指定がなく、CLSの原因になっている- すべての画像が初期ロード時に読み込まれており、ビューポート外の画像にもlazy loadingが適用されていない
ここからBad CaseとGood Caseを見ていきます。
Bad Case:生の <img> タグで画像を配置していた
AIが指摘した既存コードの問題箇所を再現すると、以下のような実装になっていました。
// Bad Case: 生の <img> タグで画像を配置
export function ProductCard({ product }: { product: Product }) {
return (
<div className="product-card">
<img src={`/images/products/${product.slug}.png`} alt={product.name} />
<h3>{product.name}</h3>
<p>{product.price.toLocaleString()}円</p>
</div>
);
}一見すると普通のコードですが、パフォーマンス観点では3つの問題があります。
1つ目は、widthとheightが指定されていない点です。ブラウザは画像のダウンロードが完了するまでレイアウト領域を確保できません。画像が読み込まれた瞬間にページ全体がガタッとずれる。CLSが0.15まで悪化していた直接の原因です。
2つ目は、Next.jsの<Image>コンポーネントを使っていない点です。Next.jsには画像のリサイズ、フォーマット変換(WebP/AVIF)、レスポンシブ対応を自動で行う仕組みがあります。生の<img>タグではこれらが一切動きません。
3つ目は、すべての画像が同時にロードされる点です。画面に見えていない商品カードの画像も初期ロードに含まれるため、LCPの画像が後回しになっていました。
Good Case:next/image で最適化した実装
AIの提案をベースに修正したコードがこちらです。
// Good Case: next/image で最適化
import Image from 'next/image';
export function ProductCard({ product }: { product: Product }) {
return (
<div className="product-card">
<Image
src={`/images/products/${product.slug}.png`}
alt={product.name}
width={640}
height={800}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
placeholder="blur"
blurDataURL={product.blurDataUrl}
/>
<h3>{product.name}</h3>
<p>{product.price.toLocaleString()}円</p>
</div>
);
}変更点を整理します。
widthとheightを明示することで、ブラウザがレイアウト領域を事前に確保します。画像の読み込み前後でレイアウトがずれなくなり、CLSが改善します。
sizes属性でビューポート幅に応じた画像サイズを指定しています。モバイルでは画面幅いっぱい、タブレットでは50%、デスクトップでは33%の幅で表示されるため、必要以上に大きな画像をダウンロードしなくなります。
placeholder="blur"とblurDataURLを組み合わせることで、画像の読み込み中にぼかしたプレビューを表示します。ユーザー体験として「何かが表示されている」状態を作れるため、体感的な待ち時間が短くなります。
ファーストビューの画像には priority を指定する
ここで、AIが最初の提案では見落としていた部分があります。商品一覧ページのファーストビューに表示される画像にpriorityプロパティを指定していなかったのです。
Next.jsの<Image>コンポーネントは、デフォルトでlazy loading(遅延読み込み)が有効です。ビューポート外の画像を後から読み込むことで初期ロードを軽くする仕組みですが、ファーストビューの画像まで遅延させてしまうとLCPが遅くなります。
私がAIの出力に手を加えた部分がこちらです。
// ファーストビュー用:priority を指定してプリロードする
import Image from 'next/image';
type ProductGridProps = {
products: Product[];
};
export function ProductGrid({ products }: ProductGridProps) {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{products.map((product, index) => (
<div key={product.id} className="product-card">
<Image
src={`/images/products/${product.slug}.png`}
alt={product.name}
width={640}
height={800}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
priority={index < 3}
placeholder="blur"
blurDataURL={product.blurDataUrl}
/>
<h3>{product.name}</h3>
<p>{product.price.toLocaleString()}円</p>
</div>
))}
</div>
);
}priority={index < 3}で、最初の3枚だけプリロード対象にしています。ファーストビューに見える可能性がある画像だけを優先的に読み込み、残りはlazy loadingに任せます。この判断はAIに「ファーストビューに何枚の商品が表示されるか」を伝えないと正確にできません。レイアウトの知識が必要な部分は、人間が判断する領域です。
next.config.js で画像フォーマットとサイズを最適化する
コンポーネントの修正と合わせて、Next.jsの設定ファイルでも画像最適化のパラメータを調整しました。
// next.config.ts
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200],
imageSizes: [16, 32, 48, 64, 96, 128, 256],
minimumCacheTTL: 60 * 60 * 24 * 30,
},
};
export default nextConfig;formatsでavifをwebpより前に指定しています。AVIFはWebPよりもさらに圧縮率が高いフォーマットです。ブラウザがAVIFに対応していればAVIFを、対応していなければWebPを、どちらも非対応ならオリジナルのフォーマットで配信します。Next.jsがこの判定を自動で行うため、開発者はフォーマット変換のコードを書く必要がありません。
deviceSizesはデフォルトから不要なサイズを削っています。実際のデバイス幅に合わせてバリエーションを絞ることで、キャッシュ効率が上がります。
minimumCacheTTLを30日(2,592,000秒)に設定しました。商品画像の差し替え頻度が月1回程度だったため、キャッシュを長めに保持する判断にしています。
blurDataURL の生成を自動化する
placeholder="blur"を使うには、各画像のぼかしプレビュー(Base64エンコードされた小さな画像)を用意する必要があります。AIにこの生成スクリプトの作成も依頼しました。
// scripts/generate-blur.ts
import { readdir } from 'fs/promises';
import { join } from 'path';
import sharp from 'sharp';
async function generateBlurDataUrls() {
const imageDir = join(process.cwd(), 'public/images/products');
const files = await readdir(imageDir);
const pngFiles = files.filter((f) => f.endsWith('.png'));
const results: Record<string, string> = {};
for (const file of pngFiles) {
const filePath = join(imageDir, file);
const buffer = await sharp(filePath).resize(10, 10).blur().toBuffer();
const base64 = `data:image/png;base64,${buffer.toString('base64')}`;
const slug = file.replace('.png', '');
results[slug] = base64;
}
console.log(JSON.stringify(results, null, 2));
}
generateBlurDataUrls();sharpライブラリで画像を10×10ピクセルにリサイズし、ぼかし処理をかけてBase64に変換しています。このスクリプトの出力をJSONファイルとして保存し、コンポーネントから参照する仕組みです。
AIが生成したこのスクリプトは、ほぼそのまま動きました。画像処理のような定型的な変換処理は、AIが正確なコードを出しやすい領域です。
改善結果:LCP 4.8秒 → 1.6秒
すべての修正を適用した結果、以下の改善が得られました。
| 指標 | Before | After | 改善幅 |
|---|---|---|---|
| LCP | 4.8秒 | 1.6秒 | 67%短縮 |
| CLS | 0.15 | 0.02 | 87%改善 |
| PageSpeed(モバイル) | 38点 | 89点 | +51点 |
| 画像転送量(トップページ) | 24MB | 1.8MB | 92%削減 |
画像転送量が24MBから1.8MBに減ったのが最も大きな変化です。AVIFフォーマットへの自動変換と、sizesによるレスポンシブ配信が効いています。
LCPの改善にはpriority指定が直接的に寄与しました。ファーストビューの画像をプリロードすることで、ブラウザの優先度制御が正しく機能するようになっています。
AI が苦手だった部分:アートディレクションと品質判断
今回のプロジェクトで、AIが適切な回答を出せなかった領域が2つあります。
1つ目はアートディレクションです。モバイルとデスクトップで画像のトリミング位置を変えたいケースがありました。たとえば商品の全体像をデスクトップで見せつつ、モバイルではロゴ部分をクローズアップするような表現です。<picture>要素と<source>を組み合わせた実装が必要ですが、「どこをトリミングするか」はデザイナーの判断であり、AIが決められるものではありません。
2つ目は画像の圧縮品質です。Next.jsのimages.qualityパラメータ(デフォルト75)をどの程度にするかは、商品のジャンルによって異なります。今回のアパレルECでは、生地の質感が伝わることが重要だったため、品質を85に設定しました。ファイルサイズと見た目のトレードオフは、実際の画像を見ながら人間が判断する領域です。
AI駆動開発では「何をAIに任せ、何を人間が判断するか」の線引きが重要です。画像フォーマットの選定やコンポーネントの書き換えはAIが得意ですが、デザインの意図や品質の許容範囲は人間が決める必要があります。
まとめ
今回は、Next.jsの画像最適化をAI駆動開発で実装したプロセスを紹介しました。
画像はWebページの転送量の大部分を占めます。とくにECサイトのように画像が主役のサイトでは、最適化の効果がCore Web Vitalsの数値に直結します。
押さえておきたいポイントは以下の3つです。
next/imageコンポーネントとsizes属性で、デバイスに応じた適切なサイズの画像を自動配信する- ファーストビューの画像には
priorityを指定し、LCPの原因となる遅延読み込みを防ぐ next.config.tsでformats: ['image/avif', 'image/webp']を設定し、モダンフォーマットへの自動変換を有効にする
実際のプロジェクトでは、これらの修正によってLCPが4.8秒から1.6秒に改善し、PageSpeedスコアは38点から89点に向上しました。
株式会社ファストコーディングでは、React/Next.jsのフロントエンド実装やAI駆動開発のご相談を承っています。画像最適化やCore Web Vitalsの改善でお困りの際は、お問い合わせフォームからお気軽にご連絡ください。

