はじめに
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 |
loading | lazy/eager | lazy |
placeholder | blur/empty | empty |
quality | 画質(1-100) | 75 |
unoptimized | 最適化スキップ | false |
loader | カスタムloader | 内蔵loader |
ベストプラクティスまとめ
- 静的インポートを優先: 自動でサイズ検出とblur生成
- sizes属性を正確に設定: 実際の表示サイズに合わせる
- LCP画像にpriority: ファーストビューの重要な画像
- fillモードの活用: レスポンシブデザインに有効
- 適切なobject-fit: cover/containを使い分け
参考文献
- Next.js Image Component API - next/imageの詳細なAPI仕様
- Next.js Image Optimization - 画像最適化の概要
- Web.dev - Optimize Images - 画像最適化のベストプラクティス
- MDN - srcset - srcset属性の詳細
- MDN - sizes - sizes属性の詳細
- Cloudinary Documentation - Cloudinaryの画像変換
- ImageKit Documentation - ImageKitの使い方