•20 min read

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

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

パフォーマンスはチェックリストではなく、継続的な実践です。高速なサイトはSEOを改善し、直帰率を減らし、コンバージョンを直接増加させます。Next.jsはほとんどの摩擦を取り除きますが、各機能がなぜ重要なのか、そしていつそれを使用すべきかを理解する必要があります。


Audio Briefing
0:00 / 0:00

重要な指標を理解する

最適化を行う前に、何を測定しているのかを知る必要があります。Core Web Vitalsは、Googleがランキングに使用する業界標準です。

指標測定内容良好な閾値影響
LCPLargest Contentful Paint — メインコンテンツが表示されるまでの時間< 2.5s体感的な読み込み速度
INPInteraction to Next Paint — 応答性< 200msユーザーインタラクティブ性
CLSCumulative Layout Shift — 視覚的な安定性< 0.1視覚的な不安定さ / 信頼性
FIDFirst Input Delay — 最初のクリック/キー入力の遅延< 100ms最初のインタラクションの感触
INPに関する注意

FIDは2024年3月にINPに置き換えられました。INPは、最初の入力だけでなく、ページライフサイクル全体の応答性を測定します。これはより大きな目標であり、意味のある最適化は両方に役立ちます。


Advertisement

最適化ツールボックス

メンタルモデル

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"
    />
  );
}

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変数

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

キャッシュとデータフロー

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で示したように動的インポートを使用する

Advertisement

しかし、それだけではありません

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
  1. すべての <img> を <Image> に置き換える — width/heightの欠落を確認する
  2. フォントの読み込みを next/font に移行する
  3. ANALYZE=true npm run build を実行し、最大のチャンクを探す
  4. コードベースで動的候補を1つ見つけ、next/dynamic に変換する

これらの4つのアクションは、バンドルサイズを30〜50%削減し、LCPを6秒から2秒未満に短縮します。

関連資料: 大規模なモノレポでのコード品質とパフォーマンスを確保するには、Vitest Monorepo Unit Testing Performance を参照してください。

こちらもおすすめ

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement
Next.js App Router動的再検証ガイド
nextjs

Next.js App Router動的再検証ガイド

Next.js App Routerのキャッシュアーキテクチャ、fetchリクエストのメモ化、revalidateTagによるデータキャッシュ無効化、オンデマンドISR再検証を習得しましょう。

Read more