はじめに
Next.jsを本番環境で効果的に運用するためには、パフォーマンスの最適化とモニタリングが欠かせません。本記事では、本番環境でのアプリケーションのパフォーマンスを向上させるための具体的な手法と、その監視方法について詳しく解説します。
この記事で学べること
- 画像最適化とコード分割による高速化
- パフォーマンスモニタリングツールの導入方法
- SSR/SSG/ISRの適切な使い分け
- Core Web Vitalsの改善テクニック
画像最適化とコード分割
next/imageによる画像最適化
Next.jsには、ビルトインの最適化機能が豊富に搭載されています。特に、next/imageコンポーネントを活用すると、画像の遅延読み込み(Lazy Loading)やデバイスに応じたリサイズが自動で行われ、ページ読み込み速度が大幅に向上します。
遅延読み込み(Lazy Loading)とは: ページの初期表示時にすべての画像を読み込むのではなく、ユーザーがスクロールして画像が表示領域に入ったタイミングで読み込む技術です。これにより、初期読み込み時間を短縮できます。
// components/OptimizedImage.tsx
import Image from 'next/image';
interface OptimizedImageProps {
src: string;
alt: string;
priority?: boolean; // ファーストビューの画像にはtrueを設定
}
export function OptimizedImage({ src, alt, priority = false }: OptimizedImageProps) {
return (
<Image
src={src}
alt={alt}
width={800}
height={600}
// 遅延読み込みはデフォルトで有効
// priorityをtrueにすると遅延読み込みが無効になりLCPが改善
priority={priority}
// 画像のサイズを最適化
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
// WebPなどの最適なフォーマットに自動変換
quality={85}
// プレースホルダーを表示してCLSを防止
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
/>
);
}
動的インポートによるコード分割
大規模なコンポーネントやライブラリは、動的インポートを使って必要なタイミングでのみ読み込むことで、初期バンドルサイズを削減できます。
// pages/dashboard.tsx
import dynamic from 'next/dynamic';
import { Suspense } from 'react';
// 重いチャートライブラリを動的にインポート
// ssr: falseでサーバーサイドレンダリングを無効化(クライアント専用コンポーネントの場合)
const DynamicChart = dynamic(() => import('@/components/Chart'), {
loading: () => <div className="animate-pulse h-64 bg-gray-200 rounded" />,
ssr: false, // クライアントサイドのみでレンダリング
});
// モーダルコンポーネントも動的インポート
const DynamicModal = dynamic(() => import('@/components/Modal'), {
loading: () => null,
});
export default function Dashboard() {
return (
<div>
<h1>ダッシュボード</h1>
{/* Suspenseと組み合わせてローディング状態を管理 */}
<Suspense fallback={<div>読み込み中...</div>}>
<DynamicChart data={chartData} />
</Suspense>
</div>
);
}
パフォーマンスのモニタリングツール
本番環境でアプリケーションの状態を監視するためには、適切なモニタリングツールの導入が重要です。
Next.jsビルトインのWeb Vitals計測
Next.jsには、Core Web Vitalsを計測するための組み込み機能があります。
// app/layout.tsx (App Router)
'use client';
import { useReportWebVitals } from 'next/web-vitals';
export function WebVitalsReporter() {
useReportWebVitals((metric) => {
// メトリクスの種類に応じて処理を分岐
switch (metric.name) {
case 'LCP': // Largest Contentful Paint(最大コンテンツの描画)
console.log('LCP:', metric.value, 'ms');
break;
case 'FID': // First Input Delay(初回入力遅延)
console.log('FID:', metric.value, 'ms');
break;
case 'CLS': // Cumulative Layout Shift(累積レイアウトシフト)
console.log('CLS:', metric.value);
break;
case 'TTFB': // Time to First Byte(最初のバイトまでの時間)
console.log('TTFB:', metric.value, 'ms');
break;
case 'INP': // Interaction to Next Paint(次の描画までのインタラクション)
console.log('INP:', metric.value, 'ms');
break;
}
// 分析サービスに送信
sendToAnalytics(metric);
});
return null;
}
// Google Analytics 4への送信例
function sendToAnalytics(metric: { name: string; value: number; id: string }) {
// gtag関数が存在する場合のみ送信
if (typeof window !== 'undefined' && 'gtag' in window) {
(window as any).gtag('event', metric.name, {
event_category: 'Web Vitals',
event_label: metric.id,
value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
non_interaction: true,
});
}
}
OpenTelemetryによる分散トレーシング
より詳細なパフォーマンス分析には、OpenTelemetryを活用します。
// instrumentation.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
export function register() {
// 本番環境でのみ有効化
if (process.env.NODE_ENV === 'production') {
const sdk = new NodeSDK({
// トレース情報の送信先を設定
traceExporter: new OTLPTraceExporter({
url: process.env.OTLP_ENDPOINT || 'http://localhost:4318/v1/traces',
}),
// 自動計装を有効化(HTTP、fetch、データベースなど)
instrumentations: [
getNodeAutoInstrumentations({
// 不要な計装は無効化してオーバーヘッドを削減
'@opentelemetry/instrumentation-fs': { enabled: false },
}),
],
});
sdk.start();
console.log('OpenTelemetry initialized');
}
}
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// OpenTelemetryの計装を有効化
experimental: {
instrumentationHook: true,
},
};
module.exports = nextConfig;
サーバーサイドレンダリング(SSR)と静的サイト生成(SSG)
SSR - リクエスト時にページを生成
SSR(Server-Side Rendering)は、リクエストごとにサーバー側でページを生成する方式です。ユーザー固有のデータや、リアルタイムで更新が必要なコンテンツに適しています。
// app/products/[id]/page.tsx (App Router)
import { notFound } from 'next/navigation';
interface Product {
id: string;
name: string;
price: number;
stock: number;
}
// 動的レンダリングを強制(リクエストごとに実行)
export const dynamic = 'force-dynamic';
async function getProduct(id: string): Promise<Product | null> {
const res = await fetch(`${process.env.API_URL}/products/${id}`, {
// キャッシュを無効化して常に最新データを取得
cache: 'no-store',
});
if (!res.ok) return null;
return res.json();
}
export default async function ProductPage({
params,
}: {
params: { id: string };
}) {
const product = await getProduct(params.id);
if (!product) {
notFound();
}
return (
<div>
<h1>{product.name}</h1>
<p>価格: {product.price.toLocaleString()}円</p>
{/* 在庫数はリアルタイムで更新される */}
<p>在庫: {product.stock}個</p>
</div>
);
}
SSG - ビルド時にページを生成
SSG(Static Site Generation)は、ビルド時に静的なHTMLファイルを生成する方式です。更新頻度が低いコンテンツに最適で、CDNからの配信により非常に高速なレスポンスを実現できます。
// app/blog/[slug]/page.tsx (App Router)
import { Metadata } from 'next';
interface Post {
slug: string;
title: string;
content: string;
publishedAt: string;
}
// ビルド時に生成するページのパスを定義
export async function generateStaticParams() {
const posts = await fetch(`${process.env.API_URL}/posts`).then((res) =>
res.json()
);
return posts.map((post: Post) => ({
slug: post.slug,
}));
}
// 静的生成を明示的に指定
export const dynamic = 'force-static';
async function getPost(slug: string): Promise<Post> {
const res = await fetch(`${process.env.API_URL}/posts/${slug}`, {
// ビルド時にキャッシュされる
cache: 'force-cache',
});
return res.json();
}
// メタデータも静的に生成
export async function generateMetadata({
params,
}: {
params: { slug: string };
}): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.content.substring(0, 160),
};
}
export default async function BlogPost({
params,
}: {
params: { slug: string };
}) {
const post = await getPost(params.slug);
return (
<article>
<h1>{post.title}</h1>
<time>{new Date(post.publishedAt).toLocaleDateString('ja-JP')}</time>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
Core Web Vitalsの最適化
Core Web Vitalsは、Googleが提供するユーザー体験を測定するための指標です。SEOにも影響するため、これらの改善は非常に重要です。
| 指標 | 説明 | 目標値 |
|---|---|---|
| LCP(Largest Contentful Paint) | 最大コンテンツの描画時間 | 2.5秒以下 |
| INP(Interaction to Next Paint) | インタラクションの応答性 | 200ms以下 |
| CLS(Cumulative Layout Shift) | レイアウトのずれ | 0.1以下 |
LCPの改善
// components/HeroSection.tsx
import Image from 'next/image';
export function HeroSection() {
return (
<section className="relative h-screen">
{/* ファーストビューの画像にはpriorityを設定 */}
<Image
src="/hero-image.jpg"
alt="ヒーロー画像"
fill
priority // LCP改善のため優先読み込み
sizes="100vw"
className="object-cover"
/>
<div className="absolute inset-0 flex items-center justify-center">
<h1 className="text-4xl font-bold text-white">
Welcome to Our Site
</h1>
</div>
</section>
);
}
CLSの改善
// components/ResponsiveImage.tsx
import Image from 'next/image';
interface ResponsiveImageProps {
src: string;
alt: string;
aspectRatio: '16/9' | '4/3' | '1/1';
}
export function ResponsiveImage({ src, alt, aspectRatio }: ResponsiveImageProps) {
// アスペクト比を指定してレイアウトシフトを防止
return (
<div
className="relative w-full"
style={{ aspectRatio }}
>
<Image
src={src}
alt={alt}
fill
className="object-cover"
sizes="(max-width: 768px) 100vw, 50vw"
/>
</div>
);
}
INPの改善
// components/InteractiveList.tsx
'use client';
import { useCallback, useTransition } from 'react';
interface Item {
id: string;
name: string;
}
interface InteractiveListProps {
items: Item[];
onItemClick: (id: string) => Promise<void>;
}
export function InteractiveList({ items, onItemClick }: InteractiveListProps) {
// useTransitionで重い処理を非ブロッキングに
const [isPending, startTransition] = useTransition();
const handleClick = useCallback((id: string) => {
// UIの応答性を維持しながら処理を実行
startTransition(async () => {
await onItemClick(id);
});
}, [onItemClick]);
return (
<ul className={isPending ? 'opacity-50' : ''}>
{items.map((item) => (
<li key={item.id}>
<button
onClick={() => handleClick(item.id)}
disabled={isPending}
className="w-full text-left p-4 hover:bg-gray-100 transition-colors"
>
{item.name}
</button>
</li>
))}
</ul>
);
}
インクリメンタル静的再生成(ISR)
ISR(Incremental Static Regeneration)は、静的生成の速度を活かしつつ、定期的にコンテンツを更新できる機能です。
// app/news/page.tsx (App Router)
interface NewsItem {
id: string;
title: string;
summary: string;
publishedAt: string;
}
// 60秒ごとにページを再生成
export const revalidate = 60;
async function getNews(): Promise<NewsItem[]> {
const res = await fetch(`${process.env.API_URL}/news`, {
// next.revalidateで再検証間隔を指定
next: { revalidate: 60 },
});
return res.json();
}
export default async function NewsPage() {
const news = await getNews();
return (
<div>
<h1>最新ニュース</h1>
{/* 最大60秒前のデータが表示される可能性がある */}
<p className="text-sm text-gray-500">
※ このページは最大60秒ごとに更新されます
</p>
<ul>
{news.map((item) => (
<li key={item.id} className="border-b py-4">
<h2 className="font-bold">{item.title}</h2>
<p>{item.summary}</p>
<time className="text-sm text-gray-400">
{new Date(item.publishedAt).toLocaleDateString('ja-JP')}
</time>
</li>
))}
</ul>
</div>
);
}
オンデマンド再検証
特定のイベント(例: CMSでの更新)をトリガーにページを再生成することも可能です。
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
// シークレットトークンで認証
const token = request.headers.get('x-revalidate-token');
if (token !== process.env.REVALIDATE_SECRET) {
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
}
const { path, tag } = await request.json();
try {
if (path) {
// 特定のパスを再検証
revalidatePath(path);
}
if (tag) {
// 特定のタグを持つキャッシュを再検証
revalidateTag(tag);
}
return NextResponse.json({
revalidated: true,
date: new Date().toISOString(),
});
} catch (error) {
return NextResponse.json(
{ error: 'Revalidation failed' },
{ status: 500 }
);
}
}
まとめ
本番環境でNext.jsを運用する際は、以下の最適化技術やツールを活用し、ユーザーに最高のパフォーマンスを提供しましょう。
パフォーマンス最適化のチェックリスト
next/imageを使用して画像を最適化する- 動的インポートで初期バンドルサイズを削減する
- 適切なレンダリング戦略(SSR/SSG/ISR)を選択する
- Core Web Vitals(LCP、INP、CLS)を継続的に監視する
- OpenTelemetryやGoogle Analyticsでメトリクスを収集する
これらの技術を適切に組み合わせることで、高速で信頼性の高いWebアプリケーションを構築できます。