Next.js 画像最適化で LCP・CLS を改善する手順

Next.js 画像最適化で LCP・CLS を改善する手順

※本記事にはプロモーション(広告・アフィリエイトリンク)を含みます。

ECサイトやメディアサイトで PageSpeed Insights のスコアに影響する要因として、画像が挙げられることが多い。Next.js には next/image コンポーネントが用意されており、設定を整えるだけで LCP(Largest Contentful Paint)と CLS(Cumulative Layout Shift)の両方を改善できる。自作 EC サイトでは商品画像を public/ ではなく route handler 経由で配信する構成にしているが、そのケースを含めた設定手順をまとめておく。

画像が Core Web Vitals に与える影響

Core Web Vitals は Google が定義する3つのユーザー体験指標で、画像に直結するのは LCP と CLS の2つだ。

公式ドキュメントによると、各指標の閾値は以下の通り(執筆時点)。

指標 Good Needs Improvement Poor
LCP 2.5秒以内 2.5〜4.0秒 4.0秒超
CLS 0.1未満 0.1〜0.25 0.25超

LCP はページ内で最大の要素(多くの場合メイン画像)が表示されるまでの時間。EC サイトでは商品のトップ画像が LCP 要素になりやすく、ファイルサイズが大きいまま配信していると値が悪化する。

CLS はレイアウトシフト。画像の縦横サイズを HTML 上で指定していないと、画像の読み込み完了時に周囲のテキストやボタンが動く。これが CLS の典型的な発生源だ。

next/image が自動でやってくれること

img タグの代わりに next/image を使うと、以下が自動化される。

  • フォーマット変換:ブラウザが対応していれば WebP または AVIF に変換して配信する
  • 遅延読み込み:viewport 外の画像は loading="lazy" がデフォルトで付く
  • レスポンシブ srcset:デバイスの解像度と画面幅に応じた複数サイズの画像を自動生成する
  • レイアウト領域の確保:width と height を指定すると読み込み前から領域を確保し、CLS を防ぐ
import Image from 'next/image'

<Image
  src="/products/item-001.jpg"
  alt="商品名"
  width={800}
  height={600}
/>

public/ 内の静的ファイルであれば、このままで動く。

route handler 経由の画像に next/image を組み合わせる

セキュリティや管理上の理由から、商品画像を public/ ではなく任意のディレクトリに置き、Next.js の route handler で配信する構成がある。自作 EC サイトでもこの構成を使っており、SD WebUI API で生成した商品写真も同じ経路で配信できる。

同一オリジン(/api/images/... のような相対パス)であれば、remotePatterns の追加設定は不要だ。src に相対パスを渡すだけで next/image が動く。

// app/api/images/[filename]/route.ts
import { NextRequest, NextResponse } from 'next/server'
import path from 'path'
import fs from 'fs/promises'

export async function GET(
  request: NextRequest,
  { params }: { params: Promise<{ filename: string }> }
) {
  const { filename } = await params
  const safe = path.basename(filename)  // ディレクトリトラバーサル対策
  const filePath = path.join(process.cwd(), 'product-images', safe)
  const file = await fs.readFile(filePath)
  return new NextResponse(file, {
    headers: {
      'Content-Type': 'image/jpeg',
      'Cache-Control': 'public, max-age=86400, stale-while-revalidate=3600',
    },
  })
}
// コンポーネント側
<Image
  src={`/api/images/${product.filename}`}
  alt={product.name}
  width={800}
  height={600}
/>

Cache-Control ヘッダーを route handler 側で明示しておくと、next/image の画像最適化キャッシュと合わせてブラウザ側でも再取得を抑えられる。

画像サイズが不定の場合は fill を使う

route handler が返す画像の縦横サイズを事前に知る方法がない場合は、fill プロパティを使う。コンテナ要素に position: relative と縦横比を設定しておく必要がある。

<div style={{ position: 'relative', aspectRatio: '4/3' }}>
  <Image
    src={`/api/images/${product.filename}`}
    alt={product.name}
    fill
    style={{ objectFit: 'cover' }}
  />
</div>

fill を使う場合はコンテナの aspect-ratio を CSS で固定しないと CLS が発生する。コンテナに明示的な高さを設定し忘れると画像が表示されない点にも注意が必要だ。

priority と sizes で LCP をさらに改善する

priority プロパティ

ページを開いたときに最初に見える(above-the-fold の)画像には priority を付ける。これで loading="lazy" が解除され、<link rel="preload"> タグが自動挿入されてブラウザが早期にフェッチを開始する。

<Image
  src="/hero.jpg"
  alt="メインビジュアル"
  width={1200}
  height={630}
  priority
/>

priority を複数の画像に付けすぎると逆効果になる。LCP 要素になりうる画像1〜2枚に絞るのが基本だ。すべての商品画像に付けてしまうと、ブラウザが一度に大量のリクエストを送り、かえって最初の表示が遅くなる。

sizes プロパティ

デフォルトでは next/image は 100vw(画面幅いっぱい)として srcset を生成する。グリッドレイアウトやサイドバーがあるページでは、実際の表示幅より大きな画像をダウンロードしてしまう。sizes を明示するとブラウザが適切なサイズを選択できる。

// 3カラムグリッドの商品画像の例
<Image
  src="/product.jpg"
  alt="商品"
  width={400}
  height={400}
  sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
/>

この指定により、スマートフォンでは画面幅いっぱい、タブレットでは1/2、PC では1/3として srcset が選択される。

next.config.ts の画像設定

// next.config.ts(執筆時点・Next.js 15 系)
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  images: {
    // 外部ドメインを使う場合のみ追加する
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'example.com',
        pathname: '/images/**',
      },
    ],
    formats: ['image/avif', 'image/webp'],
    minimumCacheTTL: 60 * 60 * 24, // デフォルトの60秒から24時間に延ばす
  },
}

export default nextConfig

公式ドキュメントによると、next/image が生成した最適化済み画像はデフォルトで60秒間キャッシュされる(執筆時点)。画像が頻繁に更新されないサイトでは minimumCacheTTL を延ばしておくと、同じ画像を繰り返し変換するサーバー負荷を減らせる。

formats に 'image/avif' を先に指定すると AVIF が優先される。AVIF は WebP より圧縮率が高い一方でエンコードに時間がかかる。サーバースペックが限られている場合は ['image/webp'] だけにする方が安定する可能性がある。

まとめ

next/image で LCP と CLS を改善するための設定を整理する。

設定 効果
width / height CLS をゼロにする(サイズ不明なら fill + コンテナの aspect-ratio)
priority LCP 要素の早期フェッチ(1〜2枚に絞る)
sizes グリッドレイアウトで無駄なダウンロードを防ぐ
同一オリジン route handler src に相対パスを渡すだけで動く
minimumCacheTTL サーバー側の変換処理の繰り返しを削減

次のアクションは、本番 URL を PageSpeed Insights(pagespeed.web.dev)に入力して実際の Core Web Vitals を計測することだ。Lighthouse のローカル計測と本番環境での実測値はしばしば乖離があるため、本番 URL を通した数値を確認してから優先順位を決めると無駄がない。

この記事で触れたもの

よくある質問

next/image と img タグの違いは何ですか?

next/image は WebP/AVIF 変換・遅延読み込み・レスポンシブ srcset 生成・CLS 防止のためのサイズ確保を自動でおこないます。img タグではこれらをすべて手動で実装する必要があります。

route handler で返す画像に next/image は使えますか?

同一オリジンの route handler であれば remotePatterns の設定は不要で、src に相対パス(例:/api/images/xxx)を渡すだけで使えます。外部ドメインの場合のみ next.config.ts の remotePatterns への追加が必要です。

priority を全商品画像に付けてよいですか?

逆効果になります。priority を付けると遅延読み込みが解除され、すべて同時フェッチが試みられます。ページ最初に見える LCP 要素の画像1〜2枚に絞るのが基本です。