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
で実測値を収集する
useReportWebVitalsNext.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/imageNext.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', });
過剰なキャッシュは情報の鮮度を損ない、過小なキャッシュは速度を損ないます。ページの性質ごとに鮮度と速度のバランスを設計することが重要です。
改善のサイクルを回す
最適化は一度やって終わりではありません。次のサイクルを継続的に回します。
- 計測: PageSpeed Insightsと実測値(RUM)で現状を把握する
- 特定: どの指標が悪いか、原因はどの要素かを切り分ける
- 改善: 画像・コード分割・キャッシュなど、効果の大きい箇所から手を入れる
- 検証: 改善前後で数値を比較し、効果を確認する
- 監視: リリース後もフィールドデータで悪化を検知する
CIにLighthouseを組み込み、しきい値を割ったらアラートを出す仕組みを作ると、パフォーマンスのデグレを早期に防げます。
まとめ
Next.jsのパフォーマンス最適化は、魔法のような一手ではなく、計測に基づく地道な積み重ねです。
- LCPは画像・フォントの最適化と優先読み込みで改善する
- CLSは領域の事前確保で防ぐ
- INPはコード分割と更新の優先度管理で改善する
- SSG/ISRとキャッシュ戦略をページの性質ごとに設計する
- 計測 → 特定 → 改善 → 検証 → 監視のサイクルを回し続ける
これらを押さえることで、ユーザー体験とSEOの両面で確かな効果が得られます。
合同会社Vermudaは、Next.jsをはじめとするモダンフロントエンドの設計・開発から、Core Web Vitalsの改善・高速化までを一貫して支援しています。表示速度や保守性に課題を感じている場合は、サービス紹介やお問い合わせよりお気軽にご相談ください。
