← ブログ一覧

Web アプリのパフォーマンス改善チェックリスト

Core Web Vitals を改善する実務手順を解説。画像最適化・バンドルサイズ削減・キャッシュ戦略・レンダリング最適化まで、受託開発で即使えるパフォーマンス改善ガイド。

#Web#パフォーマンス#Next.js#技術解説
Web アプリのパフォーマンス改善チェックリスト

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.jsmodularizeImports を設定(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 を適切に設定(ほとんどは afterInteractivelazyOnload
  • [ ] 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 パフォーマンス改善は、一度やって終わりではなく、継続的な取り組みが必要です。本記事で紹介したチェックリストを活用し、以下の優先順位で進めましょう。

すぐやるべきこと:

  1. 画像を WebP に変換し、Next.js Image を導入
  2. 不要なライブラリを削除・軽量化
  3. Lighthouse で現状を計測

次のステップ: 4. 動的インポートでコード分割 5. SSG/ISR でレンダリング最適化 6. キャッシュ戦略とCDN導入

長期的な取り組み: 7. Web Vitals の継続モニタリング 8. リリースごとのパフォーマンス回帰テスト 9. チーム内でのパフォーマンス改善文化の醸成

パフォーマンス改善でお困りの際は、Yureate にご相談ください。受託開発の実績を活かし、最適な改善プランをご提案します。

この内容について相談する他の記事を見る