Tips

Webパフォーマンス最適化完全ガイド - Core Web Vitals対応

kitahara-dev2025/10/3114 min read
Webパフォーマンス最適化完全ガイド - Core Web Vitals対応

Webパフォーマンス最適化完全ガイド

Webアプリケーションのパフォーマンスは、ユーザー体験、SEO、コンバージョン率に直結する最重要要素です。このガイドでは、実践的な最適化テクニックと具体的な数値目標を紹介します。

📊 目次

  1. パフォーマンス測定ツール
  2. Core Web Vitalsの詳細
  3. 画像の最適化
  4. コード分割とLazy Loading
  5. キャッシュ戦略
  6. バンドルサイズの削減
  7. レンダリングパフォーマンス
  8. ネットワーク最適化
  9. フォントの最適化
  10. 実際の改善事例

1. パフォーマンス測定ツール

Lighthouse(推奨)

Chrome DevToolsに統合された総合パフォーマンス測定ツール。

# コマンドラインから実行
npm install -g lighthouse
lighthouse https://your-site.com --view

主要指標:

  • Performance Score (0-100)
  • Accessibility Score (0-100)
  • Best Practices Score (0-100)
  • SEO Score (0-100)

Chrome DevTools Performance Tab

リアルタイムのパフォーマンス分析:

  1. DevToolsを開く (F12)
  2. Performanceタブを選択
  3. 記録開始 → ページ操作 → 記録停止
  4. フレームレート、CPUアクティビティ、ネットワーク活動を分析

WebPageTest

実際のデバイスとネットワーク条件でテスト:

URL: https://www.webpagetest.org/

設定例:

  • Location: Tokyo, Japan
  • Browser: Chrome
  • Connection: 4G (実際のモバイル環境)

パフォーマンス監視の設定

// app/layout.tsx - Web Vitalsの測定
export function reportWebVitals(metric: NextWebVitalsMetric) {
  console.log(metric);

  // Google Analyticsに送信
  if (window.gtag) {
    window.gtag('event', metric.name, {
      value: Math.round(metric.value),
      event_label: metric.id,
      non_interaction: true,
    });
  }
}

2. Core Web Vitalsの詳細

LCP (Largest Contentful Paint)

目標: 2.5秒以下

最大コンテンツの描画時間。ユーザーが実際にコンテンツを見られるまでの時間。

改善方法:

// 1. 画像の優先読み込み
<Image
  src="/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority  // LCPに影響する画像
  sizes="100vw"
/>

// 2. サーバーレスポンス時間の改善(SSG/ISR)
export const revalidate = 3600; // 1時間ごとに再検証

// 3. フォントの最適化
import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // フォールバックフォントを即座に表示
});

計測:

import { onLCP } from 'web-vitals';

onLCP(({ value, rating }) => {
  console.log('LCP:', value, rating);
  // 'good': < 2.5s, 'needs-improvement': 2.5-4s, 'poor': > 4s
});

FID (First Input Delay) → INP (Interaction to Next Paint)

目標: 100ミリ秒以下(FID) / 200ミリ秒以下(INP)

ユーザー操作への応答性。

改善方法:

// 1. 重い処理をWeb Workerで実行
// worker.ts
self.onmessage = (e) => {
  const result = heavyComputation(e.data);
  self.postMessage(result);
};

// component.tsx
const worker = new Worker(new URL('./worker.ts', import.meta.url));
worker.postMessage(data);
worker.onmessage = (e) => {
  setResult(e.data);
};

// 2. requestIdleCallbackで非緊急処理を遅延
if ('requestIdleCallback' in window) {
  requestIdleCallback(() => {
    // 優先度の低い処理
    trackAnalytics();
  });
}

// 3. デバウンス/スロットルの活用
import { debounce } from 'lodash';

const handleSearch = debounce((query: string) => {
  fetchSearchResults(query);
}, 300);

CLS (Cumulative Layout Shift)

目標: 0.1以下

視覚的な安定性。予期しないレイアウトシフトを防ぐ。

改善方法:

// 1. 画像・動画に明示的なサイズ指定
<Image
  src="/image.jpg"
  alt="Description"
  width={800}   // 必須
  height={600}  // 必須
/>

// 2. フォント読み込み中のレイアウトシフト防止
import { Inter } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  fallback: ['system-ui', 'arial'], // フォールバック指定
});

// 3. 動的コンテンツ用のスケルトンUI
{isLoading ? (
  <div className="animate-pulse">
    <div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
    <div className="h-4 bg-gray-200 rounded w-1/2"></div>
  </div>
) : (
  <p>{content}</p>
)}

// 4. 広告スペースの確保
<div className="h-[250px] bg-gray-100">
  {/* 広告が読み込まれるまでスペース確保 */}
  <AdComponent />
</div>

3. 画像の最適化

適切なフォーマット選択

フォーマット用途サイズ削減
WebP写真・イラスト全般25-35%
AVIF最新ブラウザ対応50%
JPEG写真(互換性重視)-
PNG透過性が必要な画像-
SVGアイコン・ロゴ最小

Next.js Image最適化

// 基本的な使用
<Image
  src="/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
  quality={85}  // デフォルト75、品質とサイズのバランス
  placeholder="blur"  // ぼかしプレースホルダー
  blurDataURL="data:image/jpeg;base64,..."
/>

// レスポンシブ画像
<Image
  src="/hero.jpg"
  alt="Hero"
  fill
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
  className="object-cover"
/>

// 外部画像の最適化
// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'images.example.com',
      },
    ],
    formats: ['image/avif', 'image/webp'],
  },
};

画像の遅延読み込み

<Image
  src="/below-fold.jpg"
  alt="Below fold image"
  width={800}
  height={600}
  loading="lazy"  // ビューポート外の画像を遅延読み込み
/>

画像圧縮ツール

# Sharp(プログラマティック)
npm install sharp

# 使用例
import sharp from 'sharp';

await sharp('input.jpg')
  .resize(800, 600)
  .webp({ quality: 80 })
  .toFile('output.webp');

# ImageOptim(GUI)
# TinyPNG(オンライン)
# Squoosh(Webアプリ)

4. コード分割とLazy Loading

動的インポート

// コンポーネントの動的読み込み
import dynamic from 'next/dynamic';

const HeavyChart = dynamic(() => import('@/components/HeavyChart'), {
  loading: () => <div className="animate-pulse">読み込み中...</div>,
  ssr: false, // クライアントサイドのみで読み込み
});

// 条件付き読み込み
const AdminPanel = dynamic(() => import('@/components/AdminPanel'));

function Dashboard({ isAdmin }: { isAdmin: boolean }) {
  return (
    <div>
      <h1>ダッシュボード</h1>
      {isAdmin && <AdminPanel />}
    </div>
  );
}

ルートベースのコード分割

Next.js App Routerは自動的にルートごとに分割:

app/
  page.tsx           → home.js (小)
  about/
    page.tsx         → about.js (小)
  dashboard/
    page.tsx         → dashboard.js (大)

React Suspenseの活用

import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      <h1>ページタイトル</h1>
      <Suspense fallback={<Loading />}>
        <HeavyComponent />
      </Suspense>
    </div>
  );
}

5. キャッシュ戦略

HTTPキャッシュヘッダー

// next.config.js
module.exports = {
  async headers() {
    return [
      {
        source: '/static/:path*',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=31536000, immutable',
          },
        ],
      },
      {
        source: '/:path*.{jpg,jpeg,png,gif,webp,svg}',
        headers: [
          {
            key: 'Cache-Control',
            value: 'public, max-age=86400, stale-while-revalidate=604800',
          },
        ],
      },
    ];
  },
};

Next.js Data Cache

// Static(デフォルト)- ビルド時にキャッシュ
export default async function Page() {
  const data = await fetch('https://api.example.com/data', {
    cache: 'force-cache'
  });
  return <div>{data.title}</div>;
}

// Revalidate - 定期的に再検証
export default async function Page() {
  const data = await fetch('https://api.example.com/data', {
    next: { revalidate: 3600 } // 1時間
  });
  return <div>{data.title}</div>;
}

// Dynamic - 常に最新データ
export default async function Page() {
  const data = await fetch('https://api.example.com/data', {
    cache: 'no-store'
  });
  return <div>{data.title}</div>;
}

Service Workerキャッシング

// public/sw.js
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('v1').then((cache) => {
      return cache.addAll([
        '/',
        '/styles.css',
        '/script.js',
      ]);
    })
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    })
  );
});

6. バンドルサイズの削減

バンドル分析

# Next.js Bundle Analyzer
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

ツリーシェイキング

// ❌ 悪い例:ライブラリ全体をインポート
import _ from 'lodash';
const result = _.debounce(fn, 300);

// ✅ 良い例:必要な関数のみインポート
import debounce from 'lodash/debounce';
const result = debounce(fn, 300);

// ✅ さらに良い:専用ライブラリ
import { debounce } from 'lodash-es'; // ES Modules版

不要な依存関係の削除

# 依存関係の分析
npm install -g depcheck
depcheck

# 未使用パッケージの削除
npm uninstall unused-package

7. レンダリングパフォーマンス

React.memoの活用

import { memo } from 'react';

const ExpensiveComponent = memo(({ data }: { data: Data }) => {
  return <div>{/* 重い描画処理 */}</div>;
});

// propsが変更された時のみ再レンダリング

useMemoとuseCallback

import { useMemo, useCallback } from 'react';

function Component({ items }: { items: Item[] }) {
  // 重い計算結果をメモ化
  const sortedItems = useMemo(() => {
    return items.sort((a, b) => a.value - b.value);
  }, [items]);

  // 関数をメモ化(子コンポーネントへの不要な再レンダリング防止)
  const handleClick = useCallback((id: string) => {
    console.log('Clicked:', id);
  }, []);

  return (
    <div>
      {sortedItems.map(item => (
        <ChildComponent key={item.id} onClick={handleClick} />
      ))}
    </div>
  );
}

仮想化(Virtualization)

// react-windowを使用
import { FixedSizeList } from 'react-window';

function VirtualList({ items }: { items: Item[] }) {
  return (
    <FixedSizeList
      height={600}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {({ index, style }) => (
        <div style={style}>
          {items[index].name}
        </div>
      )}
    </FixedSizeList>
  );
}

8. ネットワーク最適化

HTTP/2とHTTP/3

# Nginxの設定例
server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  # HTTP/3(QUIC)の有効化
  listen 443 quic reuseport;
  listen [::]:443 quic reuseport;

  ssl_protocols TLSv1.2 TLSv1.3;
}

リソースヒント

// app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html>
      <head>
        {/* DNS prefetch */}
        <link rel="dns-prefetch" href="https://api.example.com" />

        {/* Preconnect */}
        <link rel="preconnect" href="https://fonts.googleapis.com" />

        {/* Prefetch */}
        <link rel="prefetch" href="/next-page" />

        {/* Preload */}
        <link
          rel="preload"
          href="/fonts/inter.woff2"
          as="font"
          type="font/woff2"
          crossOrigin="anonymous"
        />
      </head>
      <body>{children}</body>
    </html>
  );
}

CDNの活用

// next.config.js
module.exports = {
  assetPrefix: process.env.NODE_ENV === 'production'
    ? 'https://cdn.example.com'
    : '',
};

9. フォントの最適化

Next.js Font最適化

// app/layout.tsx
import { Inter, Noto_Sans_JP } from 'next/font/google';

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-inter',
});

const notoSansJP = Noto_Sans_JP({
  weight: ['400', '700'],
  subsets: ['latin'],
  display: 'swap',
  variable: '--font-noto-sans-jp',
});

export default function RootLayout({ children }) {
  return (
    <html className={`${inter.variable} ${notoSansJP.variable}`}>
      <body>{children}</body>
    </html>
  );
}

フォントサブセット化

# pyftsubsetを使用
pip install fonttools

pyftsubset font.ttf   --text-file=characters.txt   --output-file=font-subset.woff2   --flavor=woff2

10. 実際の改善事例

Before → After: ECサイトの最適化

Before(改善前):

  • Lighthouse Score: 45/100
  • LCP: 4.2秒
  • FID: 250ms
  • CLS: 0.25
  • バンドルサイズ: 850KB

実施した施策:

  1. ✅ 画像をWebPに変換(平均40%削減)
  2. ✅ コード分割で初期バンドルを400KBに削減
  3. ✅ フォントをサブセット化(日本語フォント80%削減)
  4. ✅ 未使用CSSを削除
  5. ✅ 画像にwidth/height属性追加
  6. ✅ 重要なリソースにpreload設定

After(改善後):

  • Lighthouse Score: 92/100 (+47)
  • LCP: 1.8秒 (-2.4秒)
  • FID: 80ms (-170ms)
  • CLS: 0.05 (-0.20)
  • バンドルサイズ: 420KB (-430KB)

ビジネスインパクト:

  • ページ離脱率: 35% → 18% (-17%)
  • コンバージョン率: 2.1% → 3.4% (+1.3%)
  • 平均セッション時間: 2:15 → 3:45 (+1:30)

📝 まとめ

Webパフォーマンス最適化のチェックリスト:

  • Lighthouseで定期測定 - 目標スコア90以上
  • Core Web Vitals達成 - LCP < 2.5s, FID < 100ms, CLS < 0.1
  • 画像最適化 - WebP/AVIF、適切なサイズ、遅延読み込み
  • コード分割 - 初期バンドル < 300KB
  • キャッシュ活用 - 適切なCache-Control設定
  • レンダリング最適化 - memo、useMemo、仮想化
  • ネットワーク最適化 - HTTP/2、CDN、リソースヒント
  • フォント最適化 - サブセット化、display: swap

パフォーマンスは継続的な改善プロセスです。定期的に測定し、ユーザー体験向上を目指しましょう!

🔗 参考リンク

#Performance#Web Optimization#Core Web Vitals#Lighthouse

著者について

kitahara-devによって執筆されました。