
お好きな金額でサポートしていただけると嬉しいです!
(50円〜10,000円[税込]まで)
作成日時:2026年6月14日 22:54
更新日時:2026年6月14日 23:06
Next.js
📚 このページは「📝 Next.js パフォーマンス完全解説 — 記事構成メモ 2026」の各章トピックについて、仕組みから徹底的に解説した理解用ガイドです。記事を書く前に「なぜそうなのか」を自分の言葉で説明できるようにするためのリファレンスです。
Lighthouse などの計測ツールは「Lab データ」を返す。これは制御された環境(一定のネットワーク・CPU)でのスコアであり、現実のユーザー体験とは乖離がある。Field データ(CrUX: Chrome User Experience Report)は実際のユーザーのブラウザから収集されたデータで、地域・端末・ネットワーク環境の差が反映される。Lighthouse 満点でも CrUX の LCP が悪い、というケースが頻発する根本原因がここにある。
⚠️ よくある誤解:「Lighthouse 100点取れたからOK」→ 実際は低スペックスマホ + 3G回線のユーザーには全く改善されていない可能性がある。CrUX の75パーセンタイル値を必ず確認すること。
AI(LLM)は「動く」コードを生成することを最優先にしており、パフォーマンス上の文脈(レンダリング境界・バンドルサイズ・キャッシュ戦略)を持たない。たとえば useEffect でデータフェッチを書いてしまう、Client Component にしてしまう、next/image を使わず を使うなど。AI が書いたコードをそのまま採用すると「動くが遅い」アプリになりやすい。人間が補う役割は、パフォーマンスの文脈を設計段階から持ち込むことにある。
ブラウザがURLにアクセスしてから画面に表示されるまでのステップは以下の通り。このパイプラインを理解することが、すべての改善施策の「なぜ」につながる。
CSS プロパティを変更したとき、パイプラインのどの段階からやり直しになるかでコストが変わる。
Next.js のレンダリング戦略は「いつHTMLを生成するか」と「どこでキャッシュするか」の2軸で整理できる。App Router では fetch() のオプションでこれを制御する。
PPR はビルド時に静的シェル(HTML)と postponedState(動的部分の再開情報)の2つを生成する。リクエスト時は静的シェルをCDNから即座に返し、動的コンテンツはサーバーでレンダリングしてストリーミングする。
// next.config.ts
const nextConfig = {
experimental: {
ppr: 'incremental', // ルート単位で段階的に有効化
},
};
// app/product/[id]/page.tsx
import { Suspense } from 'react';
import { StaticProductInfo } from './StaticProductInfo'; // 静的シェル
import { DynamicPrice } from './DynamicPrice'; // 動的部分
import { UserCart } from './UserCart'; // 動的部分
export const experimental_ppr = true;
export default function ProductPage() {
return (
<div>
{/* ビルド時に静的HTML生成 → CDNから即時配信 */}
<StaticProductInfo />
{/* Suspense境界内は動的 → ストリーミングで後から流し込む */}
<Suspense fallback={<PriceSkeleton />}>
<DynamicPrice />
</Suspense>
<Suspense fallback={<CartSkeleton />}>
<UserCart />
</Suspense>
</div>
);
}
⚡ PPR の効果(実測値):TTFB 380ms → 32ms(11.9倍高速)、LCP 680ms → 65ms(10.5倍高速)。Suspense境界の設計がPPRの成否を決める。"静的なものはレイアウトに、動的なものはツリーの末端に"が鉄則。
Streaming SSR は「サーバーが全部レンダリングし終わってから送る」のではなく、「レンダリングできた部分から順次ブラウザに送る」仕組み。HTTP の chunked transfer encoding を使う。
App Router のキャッシュは4層構造:Request Memoization(リクエスト内重複排除)→ Data Cache(fetch結果のサーバーキャッシュ)→ Full Route Cache(静的ルートのHTML/RSCペイロード)→ Router Cache(クライアントのナビゲーションキャッシュ)。
// ① 静的キャッシュ(SSG相当)
const data = await fetch('/api/posts', { cache: 'force-cache' });
// ② ISR相当(60秒ごとに再検証)
const data = await fetch('/api/posts', { next: { revalidate: 60 } });
// ③ 動的(SSR相当、キャッシュなし)
const data = await fetch('/api/user', { cache: 'no-store' });
// ④ タグベース無効化(CMSと組み合わせる場合)
const data = await fetch('/api/posts', {
next: { tags: ['posts'], revalidate: 3600 }
});
// Server Actionから無効化
import { revalidateTag } from 'next/cache';
revalidateTag('posts'); // CMSのWebhookで呼ぶ
⚠️ よくあるミス:dynamic関数(cookies(), headers())を使うとそのルート全体がSSR化される。必要最小限のコンポーネントにだけdynamic関数を使い、Server Componentの境界を適切に設計することが重要。
React 16以前のレンダラーは同期的・再帰的に実行され、途中で割り込むことができなかった。Fiber は「レンダリング作業を小さなユニット(Fiber)に分割し、優先度を付けて実行できる」アーキテクチャ。連結リストベースのツリー走査を使い、ブラウザのアイドル時間に作業を分散して実行する。
Reactの re-render は以下のいずれかで発生する。「親が re-render されると子も re-render される」という連鎖構造がパフォーマンス問題の根本原因。
💣 Contextの蒋雰:Context.Providerの valueにオブジェクトリテラルを直接渡すと、親の毎 re-render ごとに新しいオブジェクト参照が生成され、すべての購読コンポーネントが毎回 re-render する。value は必ず useMemo で安定化するか、Context を読み取り専用と書き込み専用に分割すること。
メモ化はコストがゼロではない。メモ化自体のオーバーヘッド(値の比較・クロージャ保存)が re-renderが簡単なコンポーネントでは逆に遅くなる可能性がある。
React Compiler は 2025年10月に stable v1.0 としてリリースされた。ビルド時にコンポーネントを解析し、不要な re-render を山なくすためのメモ化を自動挿入する。
①実際に入力が変わる高コスト計算
②順次データフェッチのウォーターフォール問題
③バンドルサイズの問題
④Reactのルールを正しく守っていないコンポーネント(Compilerが最適化をスキップする)
// React Compiler 導入前:手動メモ化
const ExpensiveComponent = React.memo(({ data }) => {
const processed = useMemo(() => expensiveCalc(data), [data]);
const handleClick = useCallback(() => doSomething(), []);
return <div>{processed}</div>;
});
// React Compiler 導入後:そのまま書けばOK
function ExpensiveComponent({ data }) {
const processed = expensiveCalc(data); // Compilerが自動でキャッシュ
const handleClick = () => doSomething(); // Compilerが自動で安定化
return <div>{processed}</div>;
}
Long Task はメインスレッドを 50ms 以上占有するタスク。TBT(Total Blocking Time)は FCP と TTI の間に発生する全 Long Task の「50ms 超過分」の合計。TBT 0-200ms が「緑」、200-600ms が「橙」、600ms 超が「赤」。
// web-vitals で Field データを収集する例
import { onCLS, onINP, onLCP } from 'web-vitals';
export function registerWebVitals() {
onLCP((metric) => {
console.log('LCP:', metric.value, 'ms', 'ターゲット: <2500ms');
sendToAnalytics(metric);
});
onINP((metric) => {
console.log('INP:', metric.value, 'ms', 'ターゲット: <200ms');
sendToAnalytics(metric);
});
onCLS((metric) => {
console.log('CLS:', metric.value, 'ターゲット: <0.1');
sendToAnalytics(metric);
});
}
function sendToAnalytics(metric: Metric) {
fetch('/api/vitals', {
method: 'POST',
body: JSON.stringify(metric),
});
}
Webフォントは CLS の主要原因。next/font を使うとフォントをローカルにホスティングし、ビルド時に CSS 変数を生成してフィラビリティを検証する。外部への DNS ルックアップが不要になり CLS の大幅改善が期待できる。
// ✕ 非推奨:<link> で外部 Google Fonts を読み込む
// CLSが発生しやすく、外部DNS小延も発生
// ✓ 推奨:next/font でサブセットフォントをローカルホスティング
import { Noto_Sans_JP } from 'next/font/google';
const notoSansJp = Noto_Sans_JP({
subsets: ['latin'],
weight: ['400', '700'],
display: 'swap', // FOUTを許容しつつ適用する
preload: true, // LCPフォントの場合は preload: true
});
next/image は自動で WebP/AVIF 変換・レイアウトプリビュー・指定サイズでの最適化を行う。LCP 要素になる画像には priority 属性を必ず付けること。
// ✕ 非推奨:<img> で直接指定
// - 画像サイズの指定なし → CLS 発生
// - WebP変換なし → 大きなファイルサイズ
// - priorityなし → LCP遅延
<img src="/hero.jpg" alt="Hero" />
// ✓ 推奨:next/image を使う
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="ヒーロー画像"
width={1200}
height={630}
priority // LCP要素には必須:preloadリンクを生成
quality={85} // デフォルトは75。視覚的品質とファイルサイズのトレードオフ
/>
Dynamic Import を使う判断基準は「初期表示に必要か?」だけ。モーダル・ちらつきチャート・リッチテキストエディタなど、初期ビューに必要ない大きなコンポーネントに適用する。
import dynamic from 'next/dynamic';
// Chart.js のような大きなライブラリは初期画面に不要 → dynamic import
const Chart = dynamic(() => import('@/components/Chart'), {
loading: () => <ChartSkeleton />,
ssr: false, // クライアントのみrenderする場合
});
// 変換: 初期画面のバンドルサイズを大幅圧縮できる
Google Analyticsやチャットウィジェットなどのサードパーティスクリプトは INP 最大の敵の一つ。next/script の strategy を使って制御する。
import Script from 'next/script';
// afterInteractive: TTI後に読み込む(アナリティクスのデフォルト推奨)
<Script src="https://analytics.example.com/script.js" strategy="afterInteractive" />
// lazyOnload: ブラウザがアイドルになってから読み込む(チャットウィジェット等)
<Script src="https://chat.example.com/widget.js" strategy="lazyOnload" />
パフォーマンスを「一回改善したら終わり」にしないためには、PR ごとに自動計測してスコア劣化を検知することが重要。
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [push]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npm run build
- run: npm run start &
- uses: treosh/lighthouse-ci-action@v9
with:
urls: |
http://localhost:3000
http://localhost:3000/products
budgetPath: './lighthouse-budget.json'
uploadArtifacts: true
// lighthouse-budget.json (パフォーマンスバジェットの例)
[
{
"path": "/*",
"timings": [
{ "metric": "largest-contentful-paint", "budget": 2500 },
{ "metric": "total-blocking-time", "budget": 300 },
{ "metric": "cumulative-layout-shift", "budget": 0.1 }
],
"resourceSizes": [
{ "resourceType": "script", "budget": 150 },
{ "resourceType": "total", "budget": 500 }
]
}
]
AI は「動く」コードを生成するのは得意であるが、パフォーマンスの文脈(レンダリング戦略・キャッシュ設計・バンドルサイズ)は人間が設計段階から考慮する必要がある。人間が「計測 → 仮説 → 検証」のサイクルを導き、AI に実装を委譲するという分業が 2026年現在のベストプラクティス。
💡 AI時代に人間がパフォーマンスの文脈を持つことの意義:計測・診断・設計の判断は AI に委譲できない。「なぜ遅いのか」を自分の言葉で説明できる技術者が、AI が生成するコードの最後のゲートキーパーになる。