Vermudaコーポレートサイト
Tech Article

Next.jsのパフォーマンス最適化実践 — Core Web Vitalsを改善する具体策

  • Next.js
  • パフォーマンス
  • フロントエンド

Webサイトの表示速度は、ユーザー体験だけでなく検索順位やコンバージョン率にも直結します。Next.jsは標準で多くの最適化機構を備えていますが、その機能を正しく使い切れているサイトは意外と多くありません。この記事では、Core Web Vitalsを軸に、Next.jsで実際に効果のあるパフォーマンス最適化を、計測から改善まで実装例を交えて解説します。

Core Web Vitalsとは何か

Core Web Vitalsは、Googleが定義する「ユーザー体験の質」を測る3つの主要指標です。検索ランキングのシグナルにも組み込まれているため、SEOの観点でも無視できません。

指標内容良好の目安
LCP(Largest Contentful Paint)最大要素が表示されるまでの時間。読み込みの速さ2.5秒以下
CLS(Cumulative Layout Shift)表示中のレイアウトのズレ量。視覚的な安定性0.1以下
INP(Interaction to Next Paint)操作してから画面が反応するまでの時間。応答性200ms以下

INPは2024年にFID(First Input Delay)に代わって正式な指標となりました。クリックやタップへの反応の速さを、より実態に近い形で測ります。

これらは「数値を良くすること」自体が目的ではなく、体感の速さと使いやすさを定量化したものだと捉えるのが本質です。

まず計測する — 改善は計測から始まる

最適化で最もやってはいけないのが「勘で手を入れること」です。まずは現状を数値で把握します。

ラボデータとフィールドデータ

  • ラボデータ: Lighthouse や PageSpeed Insights で、決まった条件下で測った値。再現性が高く、改善検証に向く
  • フィールドデータ: 実ユーザーの環境で計測した値(CrUX)。実態を反映するが、反映に時間がかかる

両方を見ることが重要です。ラボで良好でも、実ユーザーの回線や端末では遅いことがあります。

useReportWebVitals
で実測値を収集する

Next.jsには実ユーザーのCore Web Vitalsを取得するフックが用意されています。

// app/_components/WebVitals.tsx
'use client';

import { useReportWebVitals } from 'next/web-vitals';

export function WebVitals() {
  useReportWebVitals((metric) => {
    // 計測基盤(GA4 / 自前のエンドポイントなど)へ送信
    const body = JSON.stringify(metric);
    navigator.sendBeacon('/api/vitals', body);
  });
  return null;
}

このコンポーネントをルートレイアウトに置くだけで、LCPやINPなどの実測値が継続的に集まります。改善の効果を「肌感」ではなく数値で確認できるようになります。

LCPを改善する

LCPの正体は多くの場合、ファーストビューの大きな画像やヒーローテキストです。

next/image
で画像を最適化する

Next.jsの

Image
コンポーネントは、WebP/AVIFへの自動変換、適切なサイズ配信、遅延読み込みを標準で行います。ファーストビューの主要画像には
priority
を付け、優先的に読み込ませます。

import Image from 'next/image';

export function Hero() {
  return (
    <Image
      src="/hero.jpg"
      alt="サービスのメインビジュアル"
      width={1200}
      height={630}
      priority           // LCP要素は優先読み込み
      sizes="(max-width: 768px) 100vw, 1200px"
    />
  );
}

width
と
height
(またはアスペクト比)を明示することで、後述するCLSの抑制にもつながります。

フォントの最適化

Webフォントの読み込み遅延はLCPを悪化させます。

next/font
を使うと、フォントをビルド時に取り込み、外部リクエストを排除できます。

import { Noto_Sans_JP } from 'next/font/google';

const notoSansJP = Noto_Sans_JP({
  subsets: ['latin'],
  display: 'swap', // フォント読み込み中もテキストを表示
});

display: 'swap'
により、フォント読み込み完了前でも文字が表示され、体感速度が向上します。

CLSを抑える

CLSは「読もうとしたらレイアウトがずれてボタンを押し間違えた」といった不快な体験の原因です。主な発生源と対策は次のとおりです。

  • 画像・動画: 幅・高さ、またはアスペクト比を必ず指定し、表示前から領域を確保する
  • 広告・埋め込み: あらかじめプレースホルダーの領域を確保しておく
  • Webフォント: フォント切り替え時の字形ズレを
    size-adjust
    や
    next/font
    で吸収する
  • 動的に挿入される要素: 既存コンテンツの上に後から差し込まない。
    min-height
    で枠を確保する
/* 画像の領域を事前に確保してズレを防ぐ */
.thumbnail {
  aspect-ratio: 16 / 9;
  width: 100%;
}

INPを改善する

INPはJavaScriptの実行が重いと悪化します。クリック時に長い処理が走ると、画面の更新がブロックされるためです。

コード分割と遅延読み込み

初期表示に不要なコンポーネントは

next/dynamic
で分割し、必要になったときに読み込みます。

import dynamic from 'next/dynamic';

// 重いモーダルを初期バンドルから切り離す
const HeavyModal = dynamic(() => import('./HeavyModal'), {
  ssr: false,
  loading: () => <p>読み込み中...</p>,
});

これにより初期JavaScriptが軽くなり、操作可能になるまでの時間(TTI)と応答性が改善します。

重い処理を分割する

長時間メインスレッドを占有する処理は、分割するか

requestIdleCallback
などで後回しにします。React 18以降の
useTransition
を使えば、緊急性の低い更新を区別してUIの応答性を保てます。

'use client';
import { useState, useTransition } from 'react';

export function SearchBox({ items }: { items: string[] }) {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState(items);
  const [isPending, startTransition] = useTransition();

  function handleChange(value: string) {
    setQuery(value); // 入力は即時反映(応答性を維持)
    startTransition(() => {
      // 重いフィルタリングは非緊急更新として扱う
      setResults(items.filter((i) => i.includes(value)));
    });
  }

  return (
    <div>
      <input value={query} onChange={(e) => handleChange(e.target.value)} />
      {isPending && <span>絞り込み中...</span>}
      <ul>{results.map((r) => <li key={r}>{r}</li>)}</ul>
    </div>
  );
}

レンダリング戦略 — SSG/ISRを使い分ける

Next.jsの大きな武器は、ページごとに最適なレンダリング方式を選べることです。

方式内容向くケース
SSG(静的生成)ビルド時にHTML生成内容が頻繁に変わらないページ(記事・LP)
ISR(増分静的再生成)一定間隔で再生成更新はあるが即時性は不要なページ
SSR(サーバーレンダリング)リクエストごとに生成ユーザー固有・即時性が必要なページ

ISRはAppRouterでは

revalidate
で指定します。静的配信の速さと、内容の鮮度を両立できます。

// app/blog/[slug]/page.tsx
export const revalidate = 3600; // 1時間ごとに再生成

export default async function Page({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug);
  return <article>{/* ... */}</article>;
}

可能な限りSSG/ISRに寄せることで、サーバー負荷を抑えつつLCPを大きく改善できます。

キャッシュ戦略を設計する

パフォーマンスはキャッシュ設計で大きく変わります。Next.jsでは複数のキャッシュ層が働きます。

  • データキャッシュ:
    fetch
    の結果をキャッシュし、再取得を避ける
  • フルルートキャッシュ: 静的にレンダリングされたルートをキャッシュ
  • CDNキャッシュ: 配信エッジで静的アセットをキャッシュ
// 一定時間キャッシュしつつ鮮度を保つ
const res = await fetch('https://api.example.com/data', {
  next: { revalidate: 60 }, // 60秒キャッシュ
});

// 常に最新が必要な場合はキャッシュを無効化
const live = await fetch('https://api.example.com/live', {
  cache: 'no-store',
});

過剰なキャッシュは情報の鮮度を損ない、過小なキャッシュは速度を損ないます。ページの性質ごとに鮮度と速度のバランスを設計することが重要です。

改善のサイクルを回す

最適化は一度やって終わりではありません。次のサイクルを継続的に回します。

  1. 計測: PageSpeed Insightsと実測値(RUM)で現状を把握する
  2. 特定: どの指標が悪いか、原因はどの要素かを切り分ける
  3. 改善: 画像・コード分割・キャッシュなど、効果の大きい箇所から手を入れる
  4. 検証: 改善前後で数値を比較し、効果を確認する
  5. 監視: リリース後もフィールドデータで悪化を検知する

CIにLighthouseを組み込み、しきい値を割ったらアラートを出す仕組みを作ると、パフォーマンスのデグレを早期に防げます。

まとめ

Next.jsのパフォーマンス最適化は、魔法のような一手ではなく、計測に基づく地道な積み重ねです。

  • LCPは画像・フォントの最適化と優先読み込みで改善する
  • CLSは領域の事前確保で防ぐ
  • INPはコード分割と更新の優先度管理で改善する
  • SSG/ISRとキャッシュ戦略をページの性質ごとに設計する
  • 計測 → 特定 → 改善 → 検証 → 監視のサイクルを回し続ける

これらを押さえることで、ユーザー体験とSEOの両面で確かな効果が得られます。

合同会社Vermudaは、Next.jsをはじめとするモダンフロントエンドの設計・開発から、Core Web Vitalsの改善・高速化までを一貫して支援しています。表示速度や保守性に課題を感じている場合は、サービス紹介やお問い合わせよりお気軽にご相談ください。