Documentation Next.js

はじめに

Next.js App Routerのnext/imageコンポーネントは、自動的に画像を最適化してCore Web Vitalsを改善します。本記事では、効果的な使い方と注意すべきポイントを解説します。

next/imageが行う最適化

最適化説明
フォーマット変換WebP/AVIFへの自動変換
リサイズデバイスに最適なサイズで配信
遅延読み込みビューポート外の画像を遅延ロード
プレースホルダーblur/colorプレースホルダー
キャッシュ最適化された画像のキャッシュ

基本的な使い方

静的インポート(推奨)

静的インポートを使用すると、ビルド時にNext.jsが画像の寸法を自動検出し、blur placeholderも生成します。

// components/StaticImage.tsx
import Image from 'next/image';
import profilePic from '@/public/images/profile.jpg';

export function StaticImage() {
  return (
    <Image
      src={profilePic}
      alt="プロフィール写真"
      placeholder="blur"  // 自動生成されたblurDataURLを使用
      sizes="(max-width: 768px) 100vw, 300px"
      className="rounded-lg"
    />
  );
}

動的なパス指定

APIから取得した画像URLなど、動的なパスを使用する場合は、width/heightを明示的に指定する必要があります。

// components/DynamicImage.tsx
import Image from 'next/image';

interface Props {
  src: string;
  alt: string;
  width?: number;
  height?: number;
}

export function DynamicImage({
  src,
  alt,
  width = 800,
  height = 600,
}: Props) {
  return (
    <Image
      src={src}
      alt={alt}
      width={width}
      height={height}
      sizes="(max-width: 768px) 100vw, 800px"
      className="rounded-lg"
    />
  );
}

publicディレクトリからの相対パス

publicディレクトリの画像は、パス文字列で指定します。

// 静的インポートなしの場合、width/heightが必須
<Image
  src="/images/hero.jpg"
  alt="Hero image"
  width={1920}
  height={1080}
  priority
/>

sizes属性の正しい設定

sizes属性は、ブラウザがsrcsetから適切な画像サイズを選択するために使用します。正確に設定することで、不要に大きな画像のダウンロードを防げます。

sizes属性の基本構文

// 構文: (メディア条件) サイズ, ...
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"

// 意味:
// - 画面幅640px以下: 画像はビューポート幅の100%
// - 画面幅641-1024px: 画像はビューポート幅の50%
// - それ以外: 画像はビューポート幅の33%

レスポンシブレイアウトでの設定例

// components/ResponsiveImage.tsx
import Image from 'next/image';

interface Props {
  src: string;
  alt: string;
}

export function ResponsiveImage({ src, alt }: Props) {
  return (
    <Image
      src={src}
      alt={alt}
      width={1920}
      height={1080}
      sizes="
        (max-width: 640px) 100vw,
        (max-width: 1024px) 75vw,
        (max-width: 1280px) 50vw,
        800px
      "
      priority
      className="w-full h-auto"
    />
  );
}

グリッドレイアウトでの設定

グリッドのカラム数に応じてsizesを計算します。

// components/ImageGrid.tsx
import Image from 'next/image';

interface ImageItem {
  id: string;
  src: string;
  alt: string;
}

interface Props {
  images: ImageItem[];
  columns: 2 | 3 | 4;
}

export function ImageGrid({ images, columns }: Props) {
  // カラム数に応じたsizes計算
  const sizesMap = {
    2: '(max-width: 640px) 100vw, 50vw',
    3: '(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw',
    4: '(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw',
  };

  const gridClasses = {
    2: 'grid-cols-1 md:grid-cols-2',
    3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
    4: 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4',
  };

  return (
    <div className={`grid gap-4 ${gridClasses[columns]}`}>
      {images.map((image, index) => (
        <div key={image.id} className="relative aspect-square">
          <Image
            src={image.src}
            alt={image.alt}
            fill
            sizes={sizesMap[columns]}
            className="object-cover rounded-lg"
            // 最初の行のみeager loading
            loading={index < columns ? 'eager' : 'lazy'}
          />
        </div>
      ))}
    </div>
  );
}

sizes設定のベストプラクティス

// ❌ 間違い: 常に100vw(実際の表示サイズと異なる)
<Image sizes="100vw" />

// ❌ 間違い: sizesを指定しない(デフォルトは100vw)
<Image fill />

// ✅ 正しい: 実際の表示サイズに合わせる
<Image
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

fillモードの使い方

fillモードは、親要素のサイズに合わせて画像を表示します。レスポンシブな画像表示に便利です。

基本パターン

// components/FillModeImage.tsx
import Image from 'next/image';

export function FillModeImage() {
  return (
    // ポイント1: 親要素にposition: relativeが必要
    // ポイント2: 親要素に明確な寸法が必要
    <div className="relative w-full h-64">
      <Image
        src="/images/background.jpg"
        alt="Background"
        fill
        sizes="100vw"
        className="object-cover"
      />
    </div>
  );
}

アスペクト比を維持

Tailwind CSSのaspect-*クラスを使用して、アスペクト比を維持します。

// components/AspectRatioImage.tsx
import Image from 'next/image';

interface Props {
  src: string;
  alt: string;
  aspectRatio?: '16/9' | '4/3' | '1/1' | '3/2' | '21/9';
}

export function AspectRatioImage({
  src,
  alt,
  aspectRatio = '16/9',
}: Props) {
  const aspectClasses: Record<string, string> = {
    '16/9': 'aspect-video',
    '4/3': 'aspect-[4/3]',
    '1/1': 'aspect-square',
    '3/2': 'aspect-[3/2]',
    '21/9': 'aspect-[21/9]',
  };

  return (
    <div className={`relative w-full ${aspectClasses[aspectRatio]}`}>
      <Image
        src={src}
        alt={alt}
        fill
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
        className="object-cover rounded-lg"
      />
    </div>
  );
}

object-fitの使い分け

// components/ObjectFitExamples.tsx
import Image from 'next/image';

export function ObjectFitExamples() {
  const imageSrc = '/images/sample.jpg';

  return (
    <div className="grid grid-cols-2 gap-8">
      {/* cover: 親要素を完全に覆う(はみ出し部分はトリミング) */}
      <div>
        <p className="mb-2 font-medium">object-cover</p>
        <div className="relative w-48 h-32 bg-gray-100 rounded">
          <Image
            src={imageSrc}
            alt="Cover"
            fill
            className="object-cover"
          />
        </div>
      </div>

      {/* contain: 画像全体を表示(余白が生じる場合あり) */}
      <div>
        <p className="mb-2 font-medium">object-contain</p>
        <div className="relative w-48 h-32 bg-gray-100 rounded">
          <Image
            src={imageSrc}
            alt="Contain"
            fill
            className="object-contain"
          />
        </div>
      </div>

      {/* fill: 親要素に引き伸ばし(アスペクト比が崩れる) */}
      <div>
        <p className="mb-2 font-medium">object-fill</p>
        <div className="relative w-48 h-32 bg-gray-100 rounded">
          <Image
            src={imageSrc}
            alt="Fill"
            fill
            className="object-fill"
          />
        </div>
      </div>

      {/* none: 元のサイズで中央配置 */}
      <div>
        <p className="mb-2 font-medium">object-none</p>
        <div className="relative w-48 h-32 bg-gray-100 rounded overflow-hidden">
          <Image
            src={imageSrc}
            alt="None"
            fill
            className="object-none"
          />
        </div>
      </div>
    </div>
  );
}

object-positionの活用

// 画像の位置を調整
<Image
  src="/images/portrait.jpg"
  alt="Portrait"
  fill
  className="object-cover object-top"  // 上部を優先
/>

<Image
  src="/images/product.jpg"
  alt="Product"
  fill
  className="object-cover object-center"  // 中央(デフォルト)
/>

<Image
  src="/images/landscape.jpg"
  alt="Landscape"
  fill
  className="object-cover object-bottom"  // 下部を優先
/>

カスタムLoader

外部の画像CDNを使用する場合、カスタムloaderで最適化パラメータを設定します。

Cloudinary

// lib/image-loaders.ts
import type { ImageLoaderProps } from 'next/image';

export function cloudinaryLoader({
  src,
  width,
  quality,
}: ImageLoaderProps): string {
  const params = [
    'f_auto',           // 自動フォーマット選択
    'c_limit',          // アスペクト比を維持してリサイズ
    `w_${width}`,       // 幅
    `q_${quality || 'auto'}`,  // 品質
  ];

  const cloudName = process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME;
  return `https://res.cloudinary.com/${cloudName}/image/upload/${params.join(',')}/${src}`;
}
// components/CloudinaryImage.tsx
import Image from 'next/image';
import { cloudinaryLoader } from '@/lib/image-loaders';

interface Props {
  publicId: string;
  alt: string;
  width: number;
  height: number;
}

export function CloudinaryImage({ publicId, alt, width, height }: Props) {
  return (
    <Image
      loader={cloudinaryLoader}
      src={publicId}
      alt={alt}
      width={width}
      height={height}
      quality={80}
    />
  );
}

ImageKit

// lib/image-loaders.ts
export function imagekitLoader({
  src,
  width,
  quality,
}: ImageLoaderProps): string {
  const urlEndpoint = process.env.NEXT_PUBLIC_IMAGEKIT_URL_ENDPOINT;
  const params = new URLSearchParams({
    tr: `w-${width},q-${quality || 80}`,
  });

  return `${urlEndpoint}/${src}?${params.toString()}`;
}

Imgix

// lib/image-loaders.ts
export function imgixLoader({
  src,
  width,
  quality,
}: ImageLoaderProps): string {
  const domain = process.env.NEXT_PUBLIC_IMGIX_DOMAIN;
  const params = new URLSearchParams({
    w: String(width),
    q: String(quality || 75),
    auto: 'format,compress',
  });

  return `https://${domain}/${src}?${params.toString()}`;
}

グローバルLoader設定

全ての画像に同じloaderを適用する場合は、next.config.jsで設定します。

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    loader: 'custom',
    loaderFile: './lib/image-loader.ts',
  },
};

module.exports = nextConfig;
// lib/image-loader.ts
export default function customLoader({
  src,
  width,
  quality,
}: {
  src: string;
  width: number;
  quality?: number;
}): string {
  const cdnUrl = process.env.NEXT_PUBLIC_CDN_URL;
  return `${cdnUrl}/${src}?w=${width}&q=${quality || 75}&f=auto`;
}

priority属性

使用すべき場面

priority属性を設定すると、画像がプリロードされ、LCP(Largest Contentful Paint)が改善されます。

// ✅ priorityを使うべき場面

// 1. ヒーロー画像(ファーストビュー)
<Image
  src="/images/hero.jpg"
  alt="Hero"
  fill
  priority
/>

// 2. LCP対象の画像
<Image
  src="/images/main-visual.jpg"
  alt="Main"
  width={1200}
  height={630}
  priority
/>

// 3. Above the fold(スクロールせずに見える範囲)の重要な画像
<Image
  src={user.avatar}
  alt={user.name}
  width={100}
  height={100}
  priority
/>
// ❌ priorityを使わないべき場面

// スクロールしないと見えない画像
<Image
  src="/images/footer-logo.jpg"
  alt="Logo"
  width={200}
  height={50}
  // priority は不要(デフォルトのlazy loadingで十分)
/>

// 大量の画像があるギャラリー
{images.map((img, i) => (
  <Image
    key={img.id}
    src={img.src}
    alt={img.alt}
    fill
    // 最初の1枚のみpriorityを設定
    priority={i === 0}
  />
))}

placeholder設定

blur(ローカル画像)

静的インポートした画像は、ビルド時にblurDataURLが自動生成されます。

import Image from 'next/image';
import photo from '@/public/images/photo.jpg';

export function BlurPlaceholder() {
  return (
    <Image
      src={photo}
      alt="Photo"
      placeholder="blur"
      // blurDataURLは自動生成
    />
  );
}

blur(リモート画像)

リモート画像の場合、blurDataURLを手動で設定する必要があります。

// lib/shimmer.ts
// SVGベースのシンプルなシマー効果
export function shimmer(w: number, h: number): string {
  return `
    <svg width="${w}" height="${h}" xmlns="http://www.w3.org/2000/svg">
      <defs>
        <linearGradient id="g">
          <stop stop-color="#e2e8f0" offset="0%" />
          <stop stop-color="#f8fafc" offset="50%" />
          <stop stop-color="#e2e8f0" offset="100%" />
        </linearGradient>
      </defs>
      <rect width="${w}" height="${h}" fill="#e2e8f0" />
      <rect width="${w}" height="${h}" fill="url(#g)">
        <animate
          attributeName="transform"
          attributeType="XML"
          type="translate"
          from="-${w} 0" to="${w} 0"
          dur="1s"
          repeatCount="indefinite"
        />
      </rect>
    </svg>
  `;
}

export function toBase64(str: string): string {
  return typeof window === 'undefined'
    ? Buffer.from(str).toString('base64')
    : window.btoa(str);
}
// components/RemoteImageWithBlur.tsx
import Image from 'next/image';
import { shimmer, toBase64 } from '@/lib/shimmer';

interface Props {
  src: string;
  alt: string;
  width: number;
  height: number;
}

export function RemoteImageWithBlur({ src, alt, width, height }: Props) {
  return (
    <Image
      src={src}
      alt={alt}
      width={width}
      height={height}
      placeholder="blur"
      blurDataURL={`data:image/svg+xml;base64,${toBase64(shimmer(width, height))}`}
    />
  );
}

カラープレースホルダー

// 単色のプレースホルダー
<Image
  src="https://example.com/image.jpg"
  alt="Image"
  width={800}
  height={600}
  placeholder="blur"
  blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN88P/BfwYABQAB/tnNRQwAAAAASUVORK5CYII="
/>

unoptimized属性

特定の状況で画像の最適化をスキップしたい場合に使用します。

// SVGファイル(すでに最適化されている)
<Image
  src="/images/logo.svg"
  alt="Logo"
  width={200}
  height={50}
  unoptimized
/>

// アニメーションGIF
<Image
  src="/images/animation.gif"
  alt="Animation"
  width={300}
  height={200}
  unoptimized
/>

// 外部CDNで最適化済みの画像
<Image
  src="https://optimized-cdn.com/image.jpg"
  alt="Optimized"
  width={800}
  height={600}
  unoptimized
/>

トラブルシューティング

よくあるエラーと解決方法

fillモードで親にrelativeがない

// ❌ エラー: Image with src "..." has "fill" and parent element has no position
<div>
  <Image src="/img.jpg" alt="" fill />
</div>

// ✅ 修正: 親要素にposition: relativeと寸法を設定
<div className="relative w-full h-64">
  <Image src="/img.jpg" alt="" fill />
</div>

動的srcでwidth/heightがない

// ❌ エラー: Image with src "..." must use "width" and "height" properties or "fill" property
<Image src={dynamicUrl} alt="" />

// ✅ 修正1: width/heightを指定
<Image src={dynamicUrl} alt="" width={800} height={600} />

// ✅ 修正2: fillを使用
<div className="relative h-64">
  <Image src={dynamicUrl} alt="" fill />
</div>

リモートパターン未設定

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.unsplash.com',
      },
      {
        protocol: 'https',
        hostname: '**.example.com',  // ワイルドカード
        pathname: '/images/**',       // パス制限
      },
    ],
  },
};

CLS(レイアウトシフト)が発生

// ❌ サイズ未指定でCLSが発生
<Image src={src} alt="" />

// ✅ 修正1: 事前にサイズを指定
<Image src={src} alt="" width={800} height={600} />

// ✅ 修正2: 親でアスペクト比を固定
<div className="relative aspect-video">
  <Image src={src} alt="" fill />
</div>

// ✅ 修正3: 固定の高さを設定
<div className="relative h-[300px]">
  <Image src={src} alt="" fill />
</div>

デバッグのヒント

// 開発時に画像の読み込み状態を確認
'use client';

import Image from 'next/image';
import { useState } from 'react';

export function DebugImage({ src, alt }: { src: string; alt: string }) {
  const [status, setStatus] = useState<'loading' | 'loaded' | 'error'>('loading');

  return (
    <div className="relative">
      <Image
        src={src}
        alt={alt}
        width={800}
        height={600}
        onLoadingComplete={() => setStatus('loaded')}
        onError={() => setStatus('error')}
      />
      {process.env.NODE_ENV === 'development' && (
        <span className="absolute top-2 left-2 text-xs bg-black/50 text-white px-2 py-1 rounded">
          {status}
        </span>
      )}
    </div>
  );
}

まとめ

属性用途デフォルト
src画像パス(必須)-
alt代替テキスト(必須)-
width / height画像サイズ静的インポート時は自動
fill親要素にフィットfalse
sizesレスポンシブサイズ100vw
priority優先読み込みfalse
loadinglazy/eagerlazy
placeholderblur/emptyempty
quality画質(1-100)75
unoptimized最適化スキップfalse
loaderカスタムloader内蔵loader

ベストプラクティスまとめ

  1. 静的インポートを優先: 自動でサイズ検出とblur生成
  2. sizes属性を正確に設定: 実際の表示サイズに合わせる
  3. LCP画像にpriority: ファーストビューの重要な画像
  4. fillモードの活用: レスポンシブデザインに有効
  5. 適切なobject-fit: cover/containを使い分け

参考文献

円