Next.js: 画像とフォントの最適化

最終更新:2026-08-26

画像とフォントはページサイズの 70% 以上を占めます。これらを最適化することで、LCP を 4 秒から 1 秒に短縮できます。

1. 学習目標



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 でフォントのブロッキングを排除します。

TSX
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' 遅延読み込み戦略
100%
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) レスポンシブ設定の例

TS
// 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:

TEXT 📖 参照専用
TypeScript module executes successfully.
TSX
// 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:

TEXT 📖 参照専用
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) 静的インポートの自動ぼかし

TSX
// ✅ 静的インポート — Next.js が自動的に blurDataURL を生成
import teamPhoto from '@/public/team.jpg'

export default function AboutPage() {
  return (
    <Image
      src={teamPhoto}
      alt="チーム写真"
      placeholder="blur"          // 生成されたぼかしを自動的に使用
      className="rounded-xl"
    />
  )
}

(3) リモート画像のぼかしプレビュー生成ツール

TEXT 📖 参照専用
# 使用方法: plaiceholder ライブラリ
npm install plaiceholder
TS
// 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:

TEXT 📖 参照専用
TypeScript code executed successfully.
TSX
// 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:

TEXT 📖 参照専用
Renders a list by mapping over membersWithBlur, displaying each m.


5. next/font フォント最適化

(1) フォント読み込みのパフォーマンスへの影響

100%
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 可変フォント

TSX
// 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)

TSX
// カスタムローカルフォント + 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:

TEXT 📖 参照専用
Renders the Component component UI.
TSX
// 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:

TEXT 📖 参照専用
Renders the RootLayout component UI.
CSS
/* 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 パイプライン最適化

100%
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) グローバル画像品質の設定

TS
// 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:

TEXT 📖 参照専用
TypeScript module executes successfully.
TSX
// 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:

TEXT 📖 参照専用
Renders: Quality=50
Visible text: Quality=50 | Quality=75(デフォルト) | Quality=100
💡 ヒント: 75% と 100% の差は肉眼ではほとんど区別できませんが、容量は 3 ~ 4 倍異なります。本番環境では 75~85% を推奨します。



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 画像最適化チェックリスト

TSX
// ✅ 正しい 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 の排除

TSX
// 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']
})
🔥 よくある間違い: 同じページに 3 つ以上のフォントウェイトを読み込まないでください。各バリアントは WOFF2 ファイルに約 50 KB 追加されます。

▶ サンプル: CWV 監視コンポーネント

Output:

TEXT 📖 参照専用
Renders the Component component UI.
TSX
// 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:

TEXT 📖 参照専用
Renders the WebVitalsReporter component UI.
TSX
// 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 の総合最適化

TSX
// 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>
  )
}
TSX
// 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' },
]

❓ よくある質問

Q next/imagefillwidth/height の違いは何ですか?
A fill は画像を親コンテナ(position: relative が必要)に合わせて埋め、固定サイズを指定せずに objectFit と組み合わせて使用します。静的な幅と高さは画像の寸法を明示的に指定して、占有するスペースを計算し CLS を防止します。
Q placeholder="blur" のぼかし画像を生成するにはどうすればよいですか?
A 静的インポート時は Next.js が自動的に生成します。リモート画像の場合は、手動で blurDataURL(base64 エンコードされたぼかしサムネイル)を提供する必要があります。plaiceholder ライブラリまたはオンラインツール(https://png-pixel.com などで 4x4 のぼかし画像を生成)を使用して生成できます。
Q Google Fonts の display=swapnext/fontdisplay:swap は同じですか?
A 効果は同じですが、実装が異なります。従来の方法では <link> タグに &display=swap を追加し、フォントのダウンロード完了後にのみ置き換えます。next/font はビルド時にフォントファイルをダウンロードし、CSS を介してページにインライン化するため、ランタイムリクエストがゼロになり、同時に CLS を排除する size-adjust も自動生成されます。
Q LCP 画像が自動的にプリロードされないのはなぜですか?
A LCP 画像に priority 属性が設定されているか確認してください。Next.js は priority が設定された画像にのみ <link rel="preload"> を追加します。また、remotePatterns の設定が正しくない場合も最適化が失敗する原因になります。
Q AVIF と WebP のどちらを選ぶべきですか?
A 両方を設定し、ブラウザが <picture> タグで自動的に選択するようにします。AVIF はより高い圧縮率を提供しますがデコードが遅いため、LCP 以外の画像に適しています。WebP は互換性(96%)が高く、LCP 画像に適しています。Next.js はデフォルトで両方のフォーマットを生成します。

📖 まとめ


📝 練習問題

  1. 基本問題 (⭐): プロジェクトで next/image を設定し、remotePatterns を使用して Unsplash 画像ソースを有効化し、レスポンシブ画像コンポーネント(sizesplaceholder="blur" を含む)を実装してください。

  2. 応用問題 (⭐⭐): 異なる画像品質 (50/75/100) とフォーマット (JPEG/WebP/AVIF) がページの LCP に与える影響を比較し、各組み合わせのファイルサイズと読み込み時間を記録して、最適な設定を決定してください。

  3. 発展問題 (⭐⭐⭐): 完全な CWV 最適化ソリューションを構築してください: next/font で 2 つの可変フォント(本文フォント + コードフォント)を設定し、LCP 画像に priority + blur プレースホルダーを設定し、useReportWebVitals でカスタム API に指標をレポートし、最終的に Lighthouse Performance スコア ≥ 90 を達成します。

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%