Next.jsパフォーマンスのヒント:GoodからLighthouse-Perfectへ

Table of Contents
パフォーマンスはチェックリストではなく、継続的な実践です。高速なサイトはSEOを改善し、直帰率を減らし、コンバージョンを直接増加させます。Next.jsはほとんどの摩擦を取り除きますが、各機能がなぜ重要なのか、そしていつそれを使用すべきかを理解する必要があります。
重要な指標を理解する
最適化を行う前に、何を測定しているのかを知る必要があります。Core Web Vitalsは、Googleがランキングに使用する業界標準です。
| 指標 | 測定内容 | 良好な閾値 | 影響 |
|---|---|---|---|
| LCP | Largest Contentful Paint — メインコンテンツが表示されるまでの時間 | < 2.5s | 体感的な読み込み速度 |
| INP | Interaction to Next Paint — 応答性 | < 200ms | ユーザーインタラクティブ性 |
| CLS | Cumulative Layout Shift — 視覚的な安定性 | < 0.1 | 視覚的な不安定さ / 信頼性 |
| FID | First Input Delay — 最初のクリック/キー入力の遅延 | < 100ms | 最初のインタラクションの感触 |
INPに関する注意
FIDは2024年3月にINPに置き換えられました。INPは、最初の入力だけでなく、ページライフサイクル全体の応答性を測定します。これはより大きな目標であり、意味のある最適化は両方に役立ちます。
最適化ツールボックス
メンタルモデル
Next.jsのすべてのパフォーマンス技術は、次の2つの目標のいずれかにマッピングされます。最初のピクセルを画面に速く表示するか、クライアントに送信するJavaScriptを減らすかです。これを念頭に置けば、「なぜ」が明らかになります。
1. 画像の最適化 — LCPの最大の改善点
生の <img> タグではなく、next/image の <Image> コンポーネントを使用してください。これにより、次の3つのことが自動的に行われます。
- WebP/AVIFへの自動変換 — 品質を損なうことなくファイルサイズを縮小
- サイズ/品質の制御 — モバイルでは300px、デスクトップでは1200pxの画像を配信
- レイアウトシフトの防止 — width/heightが必要なため、ブラウザが適切なスペースを確保
import Image from 'next/image';
export function Hero() {
return (
<Image
src="/hero.jpg"
alt="Dashboard overview"
width={1200}
height={675}
priority
className="rounded-xl"
/>
);
}
重要: 常にwidthとheightを指定する
両方がないと、Next.jsはレイアウトスペースを確保できず、CLSが悪化します。画像の実際の固有の寸法を使用してください。不明な場合は、OSのファイルブラウザで画像ファイルを確認するか、画像エディタを使用してください。
2. フォントの最適化 — FOITの排除
next/font モジュールを使用してください。これはビルド時にフォントをダウンロードし、ローカルで提供します。これにより、サードパーティへのリクエストがゼロになり、レイアウトシフトもゼロになります。
import { Inter, Roboto_Mono } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap', // ✅ Shows fallback text instantly
variable: '--font-inter',
});
const robotoMono = Roboto_Mono({
subsets: ['latin'],
weight: ['400', '700'],
});
これにより無料で得られるもの
- 自己ホスト型フォントファイル — Googleへの外部リクエストなし
- 自動
font-display: swap— フォントの読み込み中にテキストを表示 subsetsによるサブセット化 — 未使用のグリフを削除(例: 未使用の場合はcyrillicを省略)- 簡単なTailwind統合のためのCSS変数
import localFont from 'next/font/local';
const geist = localFont({
src: './fonts/GeistVF.woff2',
display: 'swap',
variable: '--font-geist',
});
ローカルフォントを使用する場合
すでにフォントファイル(購入済みまたは自己ホスト型)をお持ちの場合、next/font/local はGoogleのCDNを介さずに同じゼロシフトの利点を提供します。
3. 動的インポート — 重いコードを分割する
初期描画時に表示されないコンポーネントは読み込まないでください。コンポーネントレベルでコード分割するには next/dynamic を使用します。
// Heavy chart library: ~200KB gzipped
import dynamic from 'next/dynamic';
// Load it only when needed (client-side, no SSR)
const RevenueChart = dynamic(
() => import('@/components/RevenueChart'),
{
ssr: false, // ✅ Skip server rendering for this component
loading: () => <Skeleton />, // ✅ Show placeholder instantly
}
);
export function Dashboard() {
return <RevenueChart />;
}
SSRをスキップする場合(`ssr: false`)
windowまたはブラウザAPIに依存するチャート/グラフ- 重いアニメーションライブラリ(GSAP、Framer Motion)
- サードパーティウィジェット(Googleマップ、Stripe Elements)
- ダークモード切り替え — 待てよ、これはサーバーでレンダリングするのに十分軽い
動的インポートがない場合、バンドラーはすべてを単一のJSファイルにまとめることがあります。動的インポートを使用すると、コンポーネントは必要なときにのみフェッチされる別のチャンクで読み込まれます。これは、中規模から大規模なアプリにとってバンドルサイズを最も大きく削減する方法です。
バンドルアナライザー(ステップ6)を使用して、最も重いコンポーネントを見つけてください。一般的には、リッチテキストエディター、チャート、3Dビューアー、認証ウィジェットなどです。分割しすぎないでください。動的インポートはネットワークのラウンドトリップを追加します。意味のある初期JSダウンロードを妨げるコンポーネントを分割してください。
4. サーバーコンポーネント — デフォルトではJSを送信しない
App Routerでは、すべてのコンポーネントがデフォルトでサーバーコンポーネントです。これは最大のパフォーマンスレバーであり、そのコンポーネントのJSをブラウザに送信することなくレンダリングの結果を得ることができます。
// This entire file runs on the server
// → Zero JavaScript sent to the browser for this component
async function BlogPost({ slug }: { slug: string }) {
const post = await db.posts.findUnique({ where: { slug } });
return (
<article>
<h1>{post.title}</h1>
<MDXContent source={post.content} />
</article>
);
}
Need a useState, useEffect, onClick, or window?
└── Yes → Add "use client" (Client Component)
└── No → It's already a Server Component (best default)
Server Component delivers:
✅ HTML directly to browser (fast FCP)
✅ Data fetching on server (no client fetch waterfalls)
✅ Zero JS bundle cost for that component
| シナリオ | あるべき姿 | 備考 |
|---|---|---|
| ページレイアウトシェル | サーバー | インタラクティブ性は不要 |
| データフェッチングラッパー | サーバー | サーバーでクエリを実行 |
| onClickを持つボタン | クライアント | 最小限の境界 |
| useStateを持つフォーム | クライアント | フォームを小さく保ち、状態を上位に持ち上げる |
| テーマ切り替えボタン | クライアント | ボタンのみで、ページ全体ではない |
キャッシュとデータフロー
Next.jsのキャッシュレイヤー
Next.jsには複数のキャッシュメカニズムがあります。その違いを理解することで、「なぜデータが古いのか?」という混乱の90%を防ぐことができます。
リクエストのメモ化
サーバーコンポーネント内で await fetch(...) を呼び出すと、Next.jsは単一のレンダリング内で同一のリクエストを重複排除します。同じレンダリング内で2回目の同一リクエストは、ネットワークに再度アクセスすることなくキャッシュされた結果を返します。
データキャッシュ
Next.jsは fetch を組み込みのデータキャッシュで拡張します。結果は(デフォルトではほぼ永続的に)保持されます。その後のリクエストをまたがるレンダリングは、キャッシュから読み込まれます。
// Force revalidation every hour
fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // seconds
});
// No revalidation = cache until you deploy
fetch('https://api.example.com/data');
// Always fresh (no caching)
fetch('https://api.example.com/data', {
cache: 'no-store'
});
フルルートキャッシュ
静的ページはビルド時にレンダリングされ、HTMLとしてキャッシュされます。静的ページは即座に提供され、リクエスト時にサーバー計算は行われません。
ルーターキャッシュ(クライアントサイド)
クライアントサイドのルーターキャッシュは、レンダリングされたページをメモリ(および戻る/進むナビゲーションのためにlocalStorage)に保存します。ページがキャッシュから読み込まれるため、戻るナビゲーションは瞬時に感じられます。
サーバーアクションは、クライアントとサーバーの境界を曖昧にします。クライアントコンポーネントから呼び出しますが、サーバー上で完全なデータベースアクセス権限で実行されます。
'use server';
import { revalidatePath } from 'next/cache';
import { db } from '@/lib/db';
export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
await db.posts.create({ data: { title } });
revalidatePath('/posts'); // Invalidate the posts cache
}
なぜサーバーアクションなのか?
サーバーアクションが登場する前は、「投稿を作成する」には、完全なAPIルートを構築し、別のレイヤーで認証を処理し、煩雑なキャッシュタグロジックでクライアント側で再検証する必要がありました。サーバーアクションを使用すると、APIのボイラープレートやミューテーション自体のクライアント側状態管理なしに、サーバーサイドコードをインラインで記述できます。
すべてのデータが読み込まれるのを待つのではなく、準備ができたHTMLの断片をストリームします。
// Slow data component — this is the bottleneck
async function SlowChart() {
const data = await fetch('/api/slow-data').then(r => r.json());
return <Results data={data} />;
}
// Skeleton shows immediately, content streams in
export default async function DashboardPage() {
return (
<div>
<header>Dashboard</header> {/* ✅ Instant */}
<RecentActivity /> {/* ✅ Fast */}
<Suspense fallback={<ChartSkeleton />}> {/* Shows skeleton */}
<SlowChart /> {/* Streams when ready */}
</Suspense>
</div>
);
}
SSR + ストリーミングの連携方法
サーバーコンポーネントは引き続きサーバー上で実行されます。サスペンスとの違いは、準備ができたチャンクをレスポンスがストリームすることです。シェルはすぐに送信され、遅いデータは後で送信されます。ユーザーは、1つの大きなHTMLレスポンスを待つのではなく、コンテンツを段階的に見ることができます。
高度なバンドル分析
何をデプロイしているかを知る
最適化されていないインポートが1つあるだけで、クライアントバンドルに200KB以上追加される可能性があります。バンドルアナライザーは、目に見えないものを可視化します。
next/bundle-analyzerをインストールする
プロジェクトに追加します。これにより、すべてのJSチャンクのインタラクティブなツリーマップが生成されます。
npm install -D @next/bundle-analyzer
next.config.jsの設定
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({});
アナライザーを実行する
ANALYZE=true npm run build
表示されるURL(通常 http://localhost:3000/_next/analyze/...)を開きます。ツリーマップが表示され、大きなボックスほど大きなバンドルであることを示します。ホバーすると、何がスペースを占めているかを確認できます。
よくある犠牲者
| アンチパターン | 問題 | より良いアプローチ |
|---|---|---|
import { format } from 'date-fns' | ライブラリ全体(〜100以上の関数)をプルインする | date-fnsのツリーシェイキングと特定のインポートを使用するか、dayjsを使用する |
import { debounce } from 'lodash' | lodash全体(〜500KB) | lodash-esを使用するか、3行のdebounceを記述する |
import * as Icons from 'lucide-react' | すべてのアイコンをプルインするが、1つしか使用しない | 名前付きインポートを使用する: import { ChevronRight } from 'lucide-react' |
| トップレベルの重いチャートライブラリ | ファーストビューでは使用されない | ステップ3で示したように動的インポートを使用する |
しかし、それだけではありません
next/link の <Link> は、ビューポート内のリンクされたページを(ユーザーが高速接続の場合に)自動的にプリフェッチします。
import Link from 'next/link';
<Link href="/en/blog/nextjs-performance"> {/* ✅ Prefetched when visible */}
Read more
</Link>
<Link href="/heavy-page" prefetch={false}> {/* ⬇️ Skip prefetch */}
Heavy page (saves bandwidth on mobile)
</Link>
データを並列でフェッチすると、ウォーターフォールが劇的に削減されます。
// ❌ Sequential: 200ms + 300ms = 500ms total
async function SlowPage() {
const user = await fetch('/api/user').then(r => r.json()); // 200ms
const posts = await fetch('/api/posts').then(r => r.json()); // 300ms
return <Dashboard user={user} posts={posts} />;
}
// ✅ Parallel: max(200ms, 300ms) = 300ms total
async function FastPage() {
const [user, posts] = await Promise.all([
fetch('/api/user').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
]);
return <Dashboard user={user} posts={posts} />;
}
ルートセグメント設定を使用して、ページごとのレンダリング動作を制御します。
export const dynamic = 'force-static'; // Always static, even in dev
export const revalidate = 3600; // Revalidate every hour
export const fetchCache = 'force-no-store'; // Never use data cache
// For one-off routes without creating a separate layout:
export const metadata = {
title: 'My Page',
robots: { index: false }, // Prevent indexing this page
};
最適化チェックリスト
| チェック項目 | ステータス | 影響 |
|---|---|---|
どこでも <img> ではなく <Image> を使用している | published | 高 — LCP + CLS |
外部スタイルシートなしで next/font 経由でフォントを読み込んでいる | published | 高 — FCP + CLS |
| エントリバンドルに分割されていない重いライブラリがない | draft | 高 — TTI + TBT |
| クライアントコンポーネントよりもサーバーコンポーネントを使用している | draft | 高 — 総JSサイズ |
Promise.all を使用して並列データフェッチを行っている | draft | 中 — 初回読み込み時間 |
| モバイル向けにリンクプリフェッチを調整している | draft | 中 — ナビゲーション速度 |
結果の測定
推測するのではなく、測定しましょう。これらを本番環境で実行し、時間の経過とともにスコアを追跡してください。
Lighthouse CI
CIでの自動パフォーマンステスト
パフォーマンス予算を設定します。PRがLCPを閾値以下に落とすとCIが失敗します。リグレッションを防ぐために重要です。
lighthouse https://yoursite.com --budget-path=budget.json
Vercel Analytics
デプロイに組み込まれています
実際のユーザーからのリアルなWeb Vitals。実際の分布(p75、p90 LCP)を表示し、ラボの指標よりも正直です。
SpeedCurve
継続的なパフォーマンス監視
フィルムストリップビューと指標を時系列で追跡します。デプロイとパフォーマンスの変化を関連付けるのに最適です。
PageSpeed Insights
無料の公式Googleツール
迅速なラボデータとフィールドデータ。初期監査やPRの「Before」スクリーンショットをキャプチャするのに適しています。
TL;DR
今日やるべきことトップ4
- すべての
<img>を<Image>に置き換える — width/heightの欠落を確認する - フォントの読み込みを
next/fontに移行する ANALYZE=true npm run buildを実行し、最大のチャンクを探す- コードベースで動的候補を1つ見つけ、
next/dynamicに変換する
これらの4つのアクションは、バンドルサイズを30〜50%削減し、LCPを6秒から2秒未満に短縮します。
関連資料: 大規模なモノレポでのコード品質とパフォーマンスを確保するには、Vitest Monorepo Unit Testing Performance を参照してください。
- Zustand vs Jotai State Management Comparison
- Next.js App Router Dynamic Revalidation Guide
- Custom React Hook Performance Optimization Patterns
こちらもおすすめ
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Next.js App Router動的再検証ガイド
Next.js App Routerのキャッシュアーキテクチャ、fetchリクエストのメモ化、revalidateTagによるデータキャッシュ無効化、オンデマンドISR再検証を習得しましょう。
Read morePostgreSQLのVACUUMとインデックス肥大化:検知、軽減、そして自動チューニング
PostgreSQLのテーブルとインデックスの肥大化を診断・解消します。自動バキュームのチューニング方法、pg_repackによるゼロダウンタイムでの再構築、MVCCの可視性マップまでを解説します。
Read more
Redisメモリ最適化:内部構造、データ構造エンコーディング、メモリプロファイリング
RedisのRAM使用量を最大70%削減し、ziplists、listpacks、quicklists、string SDSのオーバーヘッド、自動メモリ断片化軽減策について深く掘り下げます。
Read more