
Web アプリケーションのパフォーマンスは、ユーザー体験・SEO・コンバージョン率に直結します。Google の Core Web Vitals が検索ランキング要因となった今、受託開発でも「速さ」は必須要件です。
しかし、何から手を付ければよいか、どこまで最適化すべきか、判断に迷うことも多いでしょう。本記事では、Next.js / React をベースに、実務ですぐ使えるパフォーマンス改善のチェックリストと具体的な実装例を紹介します。
1. パフォーマンス計測の基本
1.1 計測すべき指標
パフォーマンス改善は計測から始まります。主要な指標を押さえましょう。
| 指標 | 意味 | 目標値 | |------|------|--------| | LCP (Largest Contentful Paint) | メインコンテンツの表示速度 | 2.5秒以下 | | FID (First Input Delay) | 初回入力への応答速度 | 100ms以下 | | CLS (Cumulative Layout Shift) | レイアウトのズレ | 0.1以下 | | TTFB (Time To First Byte) | サーバー応答速度 | 600ms以下 | | FCP (First Contentful Paint) | 初回コンテンツ表示 | 1.8秒以下 |
1.2 計測ツールの使い分け
# Lighthouse による計測(Chrome DevTools)
# 本番環境で実施すること
# Web Vitals ライブラリの導入
npm install web-vitals
// app/layout.tsx または pages/_app.tsx
import { onCLS, onFID, onLCP, onFCP, onTTFB } from 'web-vitals';
function sendToAnalytics(metric: any) {
// Google Analytics などに送信
if (window.gtag) {
window.gtag('event', metric.name, {
value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
event_category: 'Web Vitals',
event_label: metric.id,
non_interaction: true,
});
}
}
if (typeof window !== 'undefined') {
onCLS(sendToAnalytics);
onFID(sendToAnalytics);
onLCP(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
}
実務 Tips:
- 本番環境で計測する — 開発環境は最適化されていないため、本番と乖離がある
- 複数デバイスで確認 — モバイル回線・低スペック端末でのパフォーマンスが重要
- 継続的にモニタリング — リリース後も定期的に計測し、劣化を検知する
2. 画像最適化
2.1 画像フォーマットの選択
| フォーマット | 用途 | 特徴 | |------------|------|------| | WebP | 写真・イラスト全般 | JPEG/PNG より 25-35% 小さい | | AVIF | 次世代フォーマット | WebP より更に 20% 小さいが対応ブラウザ限定 | | JPEG | 写真(フォールバック) | 広く対応 | | PNG | 透過画像 | ロゴ・アイコンに | | SVG | ベクター画像 | アイコン・図形に最適 |
2.2 Next.js Image コンポーネントの活用
// 推奨:Next.js Image コンポーネント
import Image from 'next/image';
export function ProductImage({ src, alt }: { src: string; alt: string }) {
return (
<Image
src={src}
alt={alt}
width={800}
height={600}
sizes="(max-width: 768px) 100vw, 800px"
priority={false} // Above the fold の画像のみ true にする
quality={85} // デフォルト 75、高品質が必要なら 85-90
placeholder="blur" // blurDataURL と組み合わせる
blurDataURL="data:image/jpeg;base64,..." // 省略
/>
);
}
2.3 画像の遅延読み込み
// Above the fold 以外の画像は遅延読み込み
<Image
src="/hero-bg.jpg"
alt="Hero background"
width={1920}
height={1080}
priority={true} // ファーストビューの画像のみ
/>
<Image
src="/product-detail.jpg"
alt="Product detail"
width={800}
height={600}
loading="lazy" // デフォルトで lazy だが明示的に指定も可
/>
チェックリスト:
- [ ] すべての画像を WebP または AVIF に変換済み
- [ ] Next.js Image コンポーネントを使用(または同等の最適化ライブラリ)
- [ ] ファーストビュー以外は遅延読み込み設定済み
- [ ]
sizes属性でレスポンシブ画像を適切に指定 - [ ] 不要な高解像度画像を削除済み(Retina 対応は 2x まで)
3. JavaScript バンドルサイズの削減
3.1 バンドル分析
# Next.js の場合
npm install @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
3.2 不要なライブラリの削除・代替
| 重いライブラリ | 軽量な代替 | サイズ削減 | |--------------|----------|----------| | Moment.js | date-fns | ~70KB → 2-10KB | | Lodash(全体) | Lodash-es(個別) | 70KB → 必要な分のみ | | Axios | fetch API | 13KB → 0KB | | Material-UI | Radix UI / Headless UI | ~300KB → ~50KB |
// ❌ 悪い例:ライブラリ全体をインポート
import _ from 'lodash';
import moment from 'moment';
const result = _.debounce(fn, 300);
const date = moment().format('YYYY-MM-DD');
// ✅ 良い例:必要な関数のみインポート
import debounce from 'lodash-es/debounce';
import { format } from 'date-fns';
const result = debounce(fn, 300);
const date = format(new Date(), 'yyyy-MM-dd');
3.3 動的インポート(コード分割)
// ❌ 悪い例:初回ロード時にすべて読み込み
import HeavyChart from '@/components/HeavyChart';
import AdminPanel from '@/components/AdminPanel';
export default function Dashboard({ isAdmin }: { isAdmin: boolean }) {
return (
<div>
<HeavyChart />
{isAdmin && <AdminPanel />}
</div>
);
}
// ✅ 良い例:必要なときだけ読み込み
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
loading: () => <div>Loading chart...</div>,
ssr: false, // クライアントサイドのみで使う場合
});
const AdminPanel = dynamic(() => import('@/components/AdminPanel'));
export default function Dashboard({ isAdmin }: { isAdmin: boolean }) {
return (
<div>
<HeavyChart />
{isAdmin && <AdminPanel />}
</div>
);
}
チェックリスト:
- [ ] バンドルサイズを分析し、100KB 以上のライブラリを特定済み
- [ ] 不要なライブラリを削除または軽量な代替に置き換え済み
- [ ] ファーストビューに不要なコンポーネントは動的インポート
- [ ] Tree-shaking が効くように named import を使用
- [ ]
next.config.jsでmodularizeImportsを設定(MUI など)
4. レンダリング最適化
4.1 Server Components と Client Components の使い分け
// ✅ Server Component(デフォルト)
// app/posts/[id]/page.tsx
export default async function PostPage({ params }: { params: { id: string } }) {
const post = await fetchPost(params.id); // サーバーで取得
return (
<article>
<h1>{post.title}</h1>
<PostContent content={post.content} />
<LikeButton postId={post.id} /> {/* Client Component */}
</article>
);
}
// ✅ Client Component(必要な部分のみ)
// components/LikeButton.tsx
'use client';
import { useState } from 'react';
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'} Like
</button>
);
}
4.2 静的生成(SSG)の活用
// app/blog/[slug]/page.tsx
import { getAllPosts, getPostBySlug } from '@/lib/posts';
// 静的パスを生成
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
// ビルド時にデータ取得
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await getPostBySlug(params.slug);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// 再検証(ISR)
export const revalidate = 3600; // 1時間ごとに再生成
チェックリスト:
- [ ] 静的コンテンツは SSG で生成
- [ ] 動的コンテンツでも ISR(Incremental Static Regeneration)を検討
- [ ] Client Component は最小限に(状態管理・イベントハンドラのみ)
- [ ]
use clientの配置を最適化(ツリーの下層に配置)
5. キャッシュ戦略
5.1 HTTP キャッシュヘッダーの設定
// next.config.js
module.exports = {
async headers() {
return [
{
source: '/images/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
{
source: '/_next/static/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
{
source: '/api/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, s-maxage=60, stale-while-revalidate=120',
},
],
},
];
},
};
5.2 CDN の活用
| 用途 | 推奨 CDN | 特徴 | |------|---------|------| | 静的アセット | Vercel / Cloudflare | 自動最適化・エッジキャッシュ | | 画像 | Cloudinary / Imgix | 動的リサイズ・フォーマット変換 | | 動画 | Mux / Cloudflare Stream | 適応ビットレート配信 |
チェックリスト:
- [ ] 静的アセット(JS/CSS/画像)に長期キャッシュを設定
- [ ] CDN を導入済み(Vercel / Cloudflare など)
- [ ] API レスポンスに適切な
Cache-Controlヘッダーを設定 - [ ] ファイル名にハッシュ値を含める(Next.js はデフォルトで対応)
6. フォント最適化
6.1 Next.js Font Optimization の活用
// app/layout.tsx
import { Inter, Noto_Sans_JP } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap', // FOIT を防ぐ
variable: '--font-inter',
});
const notoSansJP = Noto_Sans_JP({
subsets: ['latin'],
weight: ['400', '700'],
display: 'swap',
variable: '--font-noto-sans-jp',
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ja" className={`${inter.variable} ${notoSansJP.variable}`}>
<body>{children}</body>
</html>
);
}
/* globals.css */
body {
font-family: var(--font-noto-sans-jp), var(--font-inter), sans-serif;
}
チェックリスト:
- [ ] Google Fonts は Next.js の
next/font経由で読み込み - [ ] 使用しないウェイト・サブセットを削除
- [ ]
font-display: swapを設定してレイアウトシフトを防ぐ - [ ] カスタムフォントは
preloadで先読み
7. サードパーティスクリプトの最適化
7.1 Script コンポーネントの活用
// app/layout.tsx
import Script from 'next/script';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ja">
<body>
{children}
{/* Google Analytics */}
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"
strategy="afterInteractive" // ページロード後に実行
/>
<Script id="google-analytics" strategy="afterInteractive">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
`}
</Script>
{/* チャットウィジェット */}
<Script
src="https://cdn.example.com/chat-widget.js"
strategy="lazyOnload" // ユーザー操作後に遅延読み込み
/>
</body>
</html>
);
}
7.2 戦略の使い分け
| strategy | タイミング | 用途 |
|----------|----------|------|
| beforeInteractive | HTML パース前 | Polyfill など |
| afterInteractive | ページロード後 | GA など分析ツール |
| lazyOnload | アイドル時 | チャット・広告など |
| worker | Web Worker 内 | 重い処理の分離 |
チェックリスト:
- [ ] すべてのサードパーティスクリプトを Next.js Script で管理
- [ ] 不要なスクリプトを削除(使っていない分析ツールなど)
- [ ]
strategyを適切に設定(ほとんどはafterInteractiveかlazyOnload) - [ ] Partytown(Web Worker で実行)の導入を検討
8. 実務で使えるパフォーマンス改善フロー
8.1 優先順位付けマトリクス
| 施策 | 効果 | 実装コスト | 優先度 | |------|------|----------|--------| | 画像最適化(WebP化) | 大 | 小 | ★★★ | | Next.js Image 導入 | 大 | 中 | ★★★ | | 不要ライブラリ削除 | 大 | 小 | ★★★ | | 動的インポート | 中 | 中 | ★★☆ | | SSG/ISR 導入 | 大 | 大 | ★★☆ | | フォント最適化 | 中 | 小 | ★★☆ | | CDN 導入 | 大 | 中 | ★★☆ | | サードパーティ最適化 | 中 | 小 | ★☆☆ |
8.2 継続的改善のサイクル
# 1. 計測
npm run build
npm run start
# Lighthouse で計測(本番環境推奨)
# 2. 分析
ANALYZE=true npm run build
# バンドルサイズを確認
# 3. 改善
# 上記チェックリストに沿って実装
# 4. 検証
# 再度 Lighthouse で計測し、スコア改善を確認
# 5. モニタリング
# Web Vitals を GA に送信し、継続的に監視
まとめ
Web パフォーマンス改善は、一度やって終わりではなく、継続的な取り組みが必要です。本記事で紹介したチェックリストを活用し、以下の優先順位で進めましょう。
すぐやるべきこと:
- 画像を WebP に変換し、Next.js Image を導入
- 不要なライブラリを削除・軽量化
- Lighthouse で現状を計測
次のステップ: 4. 動的インポートでコード分割 5. SSG/ISR でレンダリング最適化 6. キャッシュ戦略とCDN導入
長期的な取り組み: 7. Web Vitals の継続モニタリング 8. リリースごとのパフォーマンス回帰テスト 9. チーム内でのパフォーマンス改善文化の醸成
パフォーマンス改善でお困りの際は、Yureate にご相談ください。受託開発の実績を活かし、最適な改善プランをご提案します。
