Next.js: 画像とフォントの最適化
最終更新:2026-08-26
画像とフォントはページサイズの 70% 以上を占めます。これらを最適化することで、LCP を 4 秒から 1 秒に短縮できます。
1. 学習目標
next/imageコンポーネントのコア属性(<Image>/remotePatterns/sizes/priority/placeholder)- 画像最適化の原理: Sharp サーバーサイド再エンコード、WebP / AVIF 適応エンコード
next/fontフォント最適化(Google Web Fonts、display:swap、size-adjust、プリロード)- Core Web Vitals (LCP / CLS / INP) のパフォーマンス最適化戦略
- リモート画像のセキュリティと
images.domainsの非推奨化と移行
2. フロントエンドパフォーマンスエンジニアの実話
(1) 課題: 1 枚の画像がページ全体のパフォーマンスを台無しにする
Diana は TaskFlow チームの DevOps エンジニアです。Dashboard ページの LCP (Largest Contentful Paint) が 4.8 秒にも達することを発見しました:
「ページ上部のバナーは 5MB の PNG 画像(元のサイズ 4000×3000px)で、直接
<img src="/banner.png" />で読み込まれています。Chrome DevTools によると、デコードに 800 ms、累積レイアウトシフト (CLS) は 0.45、フォントの読み込みがレンダリングを 600 ms ブロックしています。」
| 問題 | 影響 | CWV 指標 |
|---|---|---|
| 元画像 5MB 未圧縮 | ダウンロード 3.2s | ❌ LCP 4.8s |
| 寸法属性なし | ページレイアウトが継続的にシフト | ❌ CLS 0.45 |
| フォントレンダリングのブロック | 空白画面の延長 | ❌ FCP 2.1s |
| 非レスポンシブ画像 | モバイルでも 4K 画像を読み込み | ❌ データの浪費 |
(2) next/image + next/font の解決策
<Image>コンポーネントで画像を自動最適化し、next/fontでフォントのブロッキングを排除します。
import Image from 'next/image'
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'], display: 'swap' })
export default function Hero() {
return (
<div className={inter.className}>
<Image
src="/banner.webp"
alt="TaskFlow バナー"
width={1200}
height={400}
priority
placeholder="blur"
blurDataURL="data:image/webp;base64,..."
/>
</div>
)
}
(3) 効果
| 指標 | 最適化前 | 最適化後 | 改善 |
|---|---|---|---|
| LCP | 4.8s | 1.2s | 75% ↓ |
| CLS | 0.45 | 0.02 | 96% ↓ |
| 画像サイズ | 5 MB | 120 KB | 97% ↓ |
| FCP | 2.1s | 0.8s | 62% ↓ |
3. next/image コンポーネントのコア属性
(1) 属性リファレンス表
| 属性 | 型 | 必須 | 説明 |
|---|---|---|---|
src |
string / StaticImport | ✅ | 画像パスまたは静的インポート |
width |
number | ✅ (静的) | 画像の幅 (px) |
height |
number | ✅ (静的) | 画像の高さ (px) |
alt |
string | ✅ | 代替テキスト (アクセシビリティ) |
priority |
boolean | ❌ | LCP 画像のプリロード |
placeholder |
'blur' / 'empty' |
❌ | 読み込みプレースホルダー |
blurDataURL |
string | blur 使用時必須 |
base64 ぼかしプレースホルダー |
sizes |
string | ❌ | レスポンシブブレークポイント |
fill |
boolean | ❌ | 親コンテナを埋める |
quality |
number | ❌ | 圧縮品質 (1–100) |
loading |
'lazy' / 'eager' |
❌ | 遅延読み込み戦略 |
graph TB
A[<Image src="/photo.jpg"/>] --> B{Next.js ビルド}
B --> C[Sharp サーバーサイド再エンコード]
C --> D[WebP バージョン<br/>1200w / 800w / 400w]
C --> E[AVIF バージョン<br/>1200w / 800w / 400w]
D --> F[ブラウザが選択<br/><picture> 自動調整]
E --> F
style B fill:#cce5ff
style C fill:#d4edda
style F fill:#fff3cd
(2) レスポンシブ設定の例
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'images.unsplash.com' },
{ protocol: 'https', hostname: 'cdn.taskflow.io', port: '', pathname: '/assets/**' }
],
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 1080, 1200, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384]
}
}
export default nextConfig
images.domains は Next.js 15 以降で非推奨になりました。remotePatterns(ワイルドカードパスマッチングをサポート)を使用してください。
▶ サンプル: リモート画像 + レスポンシブ + ぼかしプレースホルダー
Output:
TypeScript module executes successfully.
// components/TeamPhoto.tsx
import Image from 'next/image'
export default function TeamPhoto() {
return (
<Image
src="https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1200"
alt="TaskFlow チーム写真"
width={1200}
height={600}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 1200px"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
priority
className="rounded-lg"
/>
)
}
Output:
Renders the TeamPhoto component UI.
sizes は異なるビューポートでの画像の表示幅をブラウザに伝え、ブラウザが最適な画像サイズを選択するのを助けます。設定しない場合、モバイルデバイスでも 1920px の画像が読み込まれる可能性があります。
4. placeholder="blur" と blurDataURL
(1) 2 つのプレースホルダー方式
| 方式 | 生成方法 | 容量 | 適用シナリオ |
|---|---|---|---|
| 静的インポートの "blur" | Next.js が自動生成 | ~2KB | ローカル画像 (import img from './photo.jpg') |
| 手動 blurDataURL | ツール生成 base64 | ~200B | リモート画像、アニメーション画像 |
| プレースホルダーライブラリ | ランタイム生成 | ~500B | 動的リモート URL が必要な場合 |
(2) 静的インポートの自動ぼかし
// ✅ 静的インポート — Next.js が自動的に blurDataURL を生成
import teamPhoto from '@/public/team.jpg'
export default function AboutPage() {
return (
<Image
src={teamPhoto}
alt="チーム写真"
placeholder="blur" // 生成されたぼかしを自動的に使用
className="rounded-xl"
/>
)
}
(3) リモート画像のぼかしプレビュー生成ツール
# 使用方法: plaiceholder ライブラリ
npm install plaiceholder
// lib/getBlurData.ts — リモート画像の blurURL 生成
import { getPlaiceholder } from 'plaiceholder'
export async function getBlurDataURL(src: string) {
try {
const response = await fetch(src)
const buffer = Buffer.from(await response.arrayBuffer())
const { base64 } = await getPlaiceholder(buffer)
return base64
} catch {
return undefined
}
}
▶ サンプル: 動的リモート画像 + ぼかしプレースホルダー
Output:
TypeScript code executed successfully.
// app/team/page.tsx — チームアバター一覧 + blur プレースホルダー
import Image from 'next/image'
import { getBlurDataURL } from '@/lib/getBlurData'
const members = [
{ name: 'Alice', avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=200' },
{ name: 'Bob', avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200' },
]
export default async function TeamPage() {
const membersWithBlur = await Promise.all(
members.map(async (m) => ({ ...m, blur: await getBlurDataURL(m.avatar) }))
)
return (
<div style={{ display: 'flex', gap: '1rem' }}>
{membersWithBlur.map((m) => (
<Image
key={m.name}
src={m.avatar}
alt={m.name}
width={100}
height={100}
placeholder="blur"
blurDataURL={m.blur}
className="rounded-full"
/>
))}
</div>
)
}
Output:
Renders a list by mapping over membersWithBlur, displaying each m.
5. next/font フォント最適化
(1) フォント読み込みのパフォーマンスへの影響
graph TB
A[従来の @font-face] --> B[レンダリングのブロック<br/>FOIT]
A --> C[レイアウトのずれ<br/>FOUT]
B --> D[FCP が 300-600ms 遅延]
C --> E[CLS 0.1-0.3]
F[next/font] --> G[display:swap<br/>代替フォントを即時使用]
F --> H[size-adjust<br/>レイアウトのずれを排除]
F --> I[preload プリロード<br/>クリティカルパスをブロックしない]
F --> J[CSS size-adjust<br/>書体メトリクスの重ね合わせ]
style A fill:#f8d7da
style F fill:#d4edda
| 問題 | 従来のフォント読み込み | next/font の解決策 |
|---|---|---|
| FOIT (フォント非表示) | フォントがダウンロードされるまでテキストが表示されない | display:swap 代替フォントを即時表示 |
| CLS (累積レイアウトシフト) | フォント読み込み前後でサイズが変化 | size-adjust フォントメトリクスを上書き |
| 追加リクエスト | 複数のフォントファイルをシリアルにダウンロード | インライン CSS + プリロード |
| Google Fonts の遅延 | CDN への遅いクロスボーダーリクエスト | ビルド時にダウンロード、ランタイムリクエストゼロ |
(2) Google 可変フォント
// app/layout.tsx — Google 可変フォント
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
preload: true,
variable: '--font-inter', // CSS 変数パターン
weight: 'variable' // 可変フォント範囲
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono'
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh" className={`${inter.variable} ${robotoMono.variable}`}>
<body style={{ fontFamily: 'var(--font-inter)' }}>
<code style={{ fontFamily: 'var(--font-roboto-mono)' }}>{children}</code>
</body>
</html>
)
}
(3) フォントサイズ調整 (size-adjust)
// カスタムローカルフォント + CLS 防止のための size-adjust
import localFont from 'next/font/local'
const myFont = localFont({
src: './fonts/CustomFont.woff2',
display: 'swap',
adjustment: {
ascent: 90,
descent: 20,
lineGap: 10,
sizeAdjust: '105%'
}
})
▶ サンプル: 複数フォント + Tailwind CSS の組み合わせ
Output:
Renders the Component component UI.
// app/layout.tsx — ビジネスフォント + コードフォント + Tailwind
import { Inter, JetBrains_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-sans',
display: 'swap'
})
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-mono',
display: 'swap'
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh" className={`${inter.variable} ${jetbrainsMono.variable}`}>
<body className="font-sans">{children}</body>
</html>
)
}
Output:
Renders the RootLayout component UI.
/* tailwind.config.ts */
import type { Config } from 'tailwindcss'
export default {
theme: {
extend: {
fontFamily: {
sans: ['var(--font-sans)'],
mono: ['var(--font-mono)']
}
}
}
} satisfies Config
6. 画像最適化の原理: Sharp + WebP/AVIF
(1) Sharp パイプライン最適化
graph LR
A[ソース画像<br/>PNG/JPEG 5MB] --> B[Sharp サーバーサイド処理]
B --> C[リサイズ<br/>1200w / 800w / 400w]
B --> D[品質圧縮<br/>75% quality]
B --> E[フォーマット変換]
E --> F[WebP<br/>~80% 容量↓]
E --> G[AVIF<br/>~90% 容量↓]
F --> H[<picture> 自動ネゴシエーション<br/>ブラウザ: 最適なフォーマットを選択]
style B fill:#d4edda
style H fill:#cce5ff
| フォーマット | 圧縮率 (JPEG 比) | ブラウザサポート | デコード速度 |
|---|---|---|---|
| JPEG | 基準 | 100% | 最速 |
| WebP | ~30% ファイルサイズ縮小 | 96% | かなり速い |
| AVIF | ~50% ファイルサイズ縮小 | 93% | やや遅い |
| HEIC | ~50% ファイルサイズ縮小 | Safari のみ | 速い |
(2) グローバル画像品質の設定
// next.config.ts — グローバル画像最適化設定
const nextConfig = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 1080, 1200, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
minimumCacheTTL: 60 * 60 * 24 * 30, // 30 日間キャッシュ
dangerouslyAllowSVG: false,
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;"
}
}
▶ サンプル: 画像品質とサイズのテスト
Output:
TypeScript module executes successfully.
// components/ImageTest.tsx — 同一画像の異なる品質レベルの比較
import Image from 'next/image'
export default function ImageTest() {
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem' }}>
<div>
<h3>Quality=50</h3>
<Image src="/test.jpg" alt="q50" width={400} height={300} quality={50} />
</div>
<div>
<h3>Quality=75(デフォルト)</h3>
<Image src="/test.jpg" alt="q75" width={400} height={300} quality={75} />
</div>
<div>
<h3>Quality=100</h3>
<Image src="/test.jpg" alt="q100" width={400} height={300} quality={100} />
</div>
</div>
)
}
Output:
Renders: Quality=50
Visible text: Quality=50 | Quality=75(デフォルト) | Quality=100
7. Core Web Vitals の実践的最適化
(1) CWV の 3 つの主要指標
| 指標 | 最大スコア | ギャップ | 最適化施策 |
|---|---|---|---|
| LCP (Largest Contentful Paint) | ≤2.5s | >4.0s | LCP 画像のプリロード + 圧縮 + CDN |
| CLS (Cumulative Layout Shift) | ≤0.1 | >0.25 | 固定画像寸法 + フォントサイズ調整 |
| INP (Interaction to Next Paint) | ≤200 ms | >500 ms | JS 実行の削減 + コード分割 |
(2) LCP 画像最適化チェックリスト
// ✅ 正しい LCP 画像設定
export default function HeroBanner() {
return (
<Image
src="/hero-banner.webp"
alt="TaskFlow ヒーロー"
width={1440}
height={600}
priority // Next.js にこの画像をプリロードするよう指示
quality={85}
placeholder="blur"
sizes="100vw"
style={{ width: '100%', height: 'auto' }} // レスポンシブ
/>
)
}
(3) フォント CLS の排除
// layout.tsx — フォント CLS 完全排除ソリューション
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
adjustFontFallback: true, // Next.js が自動的に size-adjust を計算
fallback: ['system-ui', 'sans-serif']
})
▶ サンプル: CWV 監視コンポーネント
Output:
Renders the Component component UI.
// components/WebVitals.tsx — CWV を分析プラットフォームに送信
'use client'
import { useReportWebVitals } from 'next/web-vitals'
export default function WebVitalsReporter() {
useReportWebVitals((metric) => {
console.log(metric) // 開発中に表示
// 本番環境から分析 API にレポート
if (process.env.NODE_ENV === 'production') {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
id: metric.id
})
navigator.sendBeacon('/api/analytics', body)
}
})
return null
}
Output:
Renders the WebVitalsReporter component UI.
// app/layout.tsx — 監視のマウント
import WebVitalsReporter from '@/components/WebVitals'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<WebVitalsReporter />
</body>
</html>
)
}
8. 完全なサンプル: ホームページの画像、フォント、CWV の総合最適化
// app/layout.tsx — ルートレイアウト、グローバルフォント + 画像 + Vitals 監視
import { Inter, Noto_Sans_SC } from 'next/font/google'
import WebVitalsReporter from '@/components/WebVitals'
import type { Metadata } from 'next'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
adjustFontFallback: true
})
const notoSansSC = Noto_Sans_SC({
subsets: ['latin'],
weight: 'variable',
variable: '--font-noto',
display: 'swap',
adjustFontFallback: true
})
export const metadata: Metadata = {
title: 'TaskFlow - チームコラボレーションプラットフォーム',
description: 'TaskFlow は 10,000 以上のチームの効率的なコラボレーションを支援します'
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh" className={`${inter.variable} ${notoSansSC.variable}`}>
<body style={{ fontFamily: 'var(--font-inter), var(--font-noto), sans-serif' }}>
{children}
<WebVitalsReporter />
</body>
</html>
)
}
// app/page.tsx — ホーム ヒーロー + LCP 最適化
import Image from 'next/image'
import Link from 'next/link'
export default function HomePage() {
return (
<div>
{/* ヒーロー領域 — LCP 画像 */}
<div style={{ position: 'relative', width: '100%', height: 500 }}>
<Image
src="https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1920&q=85"
alt="TaskFlow チームワーク"
fill
priority
sizes="100vw"
style={{ objectFit: 'cover' }}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
/>
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ textAlign: 'center', color: '#fff' }}>
<h1 style={{ fontSize: 'clamp(2rem, 5vw, 4rem)' }}>TaskFlow</h1>
<p style={{ fontSize: '1.25rem' }}>10,000 以上のチームが現在使用しているコラボレーションプラットフォーム</p>
<Link href="/signup" style={{ display: 'inline-block', padding: '0.75rem 2rem', background: '#4f46e5', color: '#fff', borderRadius: 8 }}>
無料で始める
</Link>
</div>
</div>
</div>
{/* 機能領域 — 画像の遅延読み込み */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '2rem', padding: '4rem 2rem' }}>
{features.map((feature) => (
<div key={feature.title}>
<Image
src={feature.icon}
alt={feature.title}
width={48}
height={48}
loading="lazy"
/>
<h3>{feature.title}</h3>
<p>{feature.description}</p>
</div>
))}
</div>
</div>
)
}
const features = [
{ title: 'プロジェクト管理', description: '掲示板 + ガントチャート', icon: '/icons/project.svg' },
{ title: 'リアルタイムコラボレーション', description: '複数ユーザーによる同時編集', icon: '/icons/team.svg' },
{ title: 'インテリジェント分析', description: 'AI によるインサイト', icon: '/icons/analytics.svg' },
]
❓ よくある質問
next/image の fill と width/height の違いは何ですか?fill は画像を親コンテナ(position: relative が必要)に合わせて埋め、固定サイズを指定せずに objectFit と組み合わせて使用します。静的な幅と高さは画像の寸法を明示的に指定して、占有するスペースを計算し CLS を防止します。placeholder="blur" のぼかし画像を生成するにはどうすればよいですか?blurDataURL(base64 エンコードされたぼかしサムネイル)を提供する必要があります。plaiceholder ライブラリまたはオンラインツール(https://png-pixel.com などで 4x4 のぼかし画像を生成)を使用して生成できます。display=swap と next/font の display:swap は同じですか?<link> タグに &display=swap を追加し、フォントのダウンロード完了後にのみ置き換えます。next/font はビルド時にフォントファイルをダウンロードし、CSS を介してページにインライン化するため、ランタイムリクエストがゼロになり、同時に CLS を排除する size-adjust も自動生成されます。priority 属性が設定されているか確認してください。Next.js は priority が設定された画像にのみ <link rel="preload"> を追加します。また、remotePatterns の設定が正しくない場合も最適化が失敗する原因になります。<picture> タグで自動的に選択するようにします。AVIF はより高い圧縮率を提供しますがデコードが遅いため、LCP 以外の画像に適しています。WebP は互換性(96%)が高く、LCP 画像に適しています。Next.js はデフォルトで両方のフォーマットを生成します。📖 まとめ
next/imageは自動最適化を提供: Sharp 再エンコード、WebP/AVIF 適応エンコード、レスポンシブマルチサイズ画像、遅延読み込みremotePatternsは非推奨のimages.domainsを置き換え、パスワイルドカードの安全なマッチングをサポートplaceholder="blur"とblurDataURLを組み合わせて、画像読み込み時の空白のちらつきを排除next/fontはビルド時に Google Fonts をダウンロードし、FOIT と FOUT を排除し、フォントメトリクスを自動調整して CLS を防止- LCP 画像はプリロードのために
priorityを設定する必要があります。ファーストビュー以外の画像はloading="lazy"で遅延読み込み - Core Web Vitals 最適化: 画像圧縮 (LCP)、固定寸法 (CLS)、JavaScript コード分割 (INP)
📝 練習問題
-
基本問題 (⭐): プロジェクトで
next/imageを設定し、remotePatternsを使用して Unsplash 画像ソースを有効化し、レスポンシブ画像コンポーネント(sizesとplaceholder="blur"を含む)を実装してください。 -
応用問題 (⭐⭐): 異なる画像品質 (50/75/100) とフォーマット (JPEG/WebP/AVIF) がページの LCP に与える影響を比較し、各組み合わせのファイルサイズと読み込み時間を記録して、最適な設定を決定してください。
-
発展問題 (⭐⭐⭐): 完全な CWV 最適化ソリューションを構築してください:
next/fontで 2 つの可変フォント(本文フォント + コードフォント)を設定し、LCP 画像にpriority+blurプレースホルダーを設定し、useReportWebVitalsでカスタム API に指標をレポートし、最終的に Lighthouse Performance スコア ≥ 90 を達成します。