T-CREATOR

Preact 本番最適化運用:Lighthouse 95 点超えのビルド設定と監視 KPI

Preact 本番最適化運用:Lighthouse 95 点超えのビルド設定と監視 KPI

Web アプリケーションのパフォーマンスは、ユーザー体験やビジネス成果に直結する重要な要素です。特に Preact のような軽量フレームワークを採用する際には、その利点を最大限に活かした最適化が求められます。

本記事では、Lighthouse で 95 点以上を安定して維持するための Preact 本番環境の最適化手法を、実践的なビルド設定と継続的な監視 KPI の設定方法を中心に解説します。実際の運用で効果が実証された具体的な設定例と、パフォーマンス劣化を早期に検知するための監視指標をご紹介しますので、すぐに実務に活かしていただけるでしょう。

背景

Preact を選択する理由

Preact は React の軽量代替フレームワークとして、わずか 3KB のサイズで React とほぼ同じ API を提供します。モバイルファーストの時代において、初期ロード時間の短縮は SEO やコンバージョン率に大きく影響するため、多くのプロジェクトで採用が進んでいますね。

しかし、フレームワークが軽量であっても、適切なビルド設定や運用体制がなければ、その恩恵を十分に受けることはできません。依存ライブラリの増加、不適切なコード分割、画像の最適化不足などにより、気づかないうちにバンドルサイズが肥大化することがあります。

Lighthouse スコアの重要性

Lighthouse は Google が提供するパフォーマンス監査ツールで、以下の 5 つの指標で Web サイトを評価します。

#カテゴリ説明
1Performanceページの読み込み速度と応答性
2Accessibilityアクセシビリティへの対応度
3Best Practicesセキュリティやモダンな実装の適用度
4SEO検索エンジン最適化の評価
5PWAプログレッシブウェブアプリの対応度

特に Performance スコアは Core Web Vitals(LCP、FID、CLS)と密接に関連しており、Google 検索ランキングの要因にもなっています。95 点以上を維持することで、優れたユーザー体験と SEO 効果の両立が実現できるのです。

以下の図は、Preact アプリケーションの基本的な構成と最適化のポイントを示しています。

mermaidflowchart TB
  source["ソースコード<br/>(Preact + TypeScript)"]
  build["ビルドプロセス<br/>(Vite/Webpack)"]
  optimize["最適化処理<br/>(minify/tree-shake/code-split)"]
  assets["静的アセット<br/>(JS/CSS/Images)"]
  cdn["CDN 配信"]
  browser["ブラウザ"]

  source -->|トランスパイル| build
  build -->|最適化| optimize
  optimize -->|生成| assets
  assets -->|配信| cdn
  cdn -->|ダウンロード| browser

  style optimize fill:#e1f5ff
  style assets fill:#fff3e0

図から分かるように、ビルドプロセスでの最適化処理が最終的な配信サイズとパフォーマンスを大きく左右します。

本番環境での課題

開発環境では快適に動作していても、本番環境では以下のような課題が発生しがちです。

まず、依存関係の肥大化が挙げられます。便利なライブラリを追加していくうちに、気づけばバンドルサイズが数 MB を超えているケースも少なくありません。

次に、画像やフォントなどの静的リソースの最適化不足です。高解像度画像をそのまま配信したり、使用していないフォントウェイトを読み込んだりすることで、ページロード時間が大幅に増加します。

また、本番環境特有の問題として、キャッシュ戦略の不備やレンダリングブロッキングリソースの存在も見過ごせません。これらは開発環境では気づきにくく、実際のユーザーがアクセスして初めて問題が顕在化することがあります。

課題

パフォーマンス劣化の主な原因

Preact アプリケーションで Lighthouse スコアが低下する主な原因を整理してみましょう。

バンドルサイズの肥大化が最も頻繁に発生する問題です。必要以上のライブラリを含めてしまったり、tree-shaking が適切に機能していなかったりすることで、JavaScript のダウンロードと解析に時間がかかります。特に、moment.js や lodash などの大きなライブラリを完全にインポートしてしまうケースが多いですね。

コード分割の不足も深刻な課題となります。すべてのコードを 1 つのバンドルにまとめてしまうと、初回アクセス時に不要なコードまでダウンロードすることになり、Time to Interactive(TTI)が悪化します。

画像最適化の欠如は見落とされがちですが、影響は甚大です。WebP や AVIF などの次世代フォーマットを使用せず、また適切なサイズにリサイズしていない画像は、Largest Contentful Paint(LCP)を大幅に悪化させます。

以下の図は、パフォーマンス劣化の因果関係を示しています。

mermaidflowchart TD
  cause1["バンドルサイズ増大"]
  cause2["コード分割不足"]
  cause3["画像最適化不足"]
  cause4["キャッシュ戦略不備"]

  effect1["ダウンロード時間増加"]
  effect2["解析時間増加"]
  effect3["LCP 悪化"]
  effect4["TTI 遅延"]

  result["Lighthouse スコア低下"]

  cause1 --> effect1
  cause2 --> effect2
  cause1 --> effect2
  cause3 --> effect3
  cause4 --> effect1

  effect1 --> result
  effect2 --> result
  effect3 --> result
  effect4 --> result

  style result fill:#ffcdd2

このように、複数の原因が相互に影響し合い、最終的なスコア低下につながることが分かります。

監視体制の不備

パフォーマンスの劣化をリアルタイムで検知できないことも大きな課題です。デプロイ後にスコアが下がっていても、次回のデプロイまで気づかないケースが多いでしょう。

継続的なパフォーマンス監視には、以下の要素が必要となります。

#要素説明
1自動測定CI/CD パイプラインでのスコア測定
2閾値設定許容できるスコアの下限値設定
3アラートスコア低下時の通知機構
4トレンド分析時系列でのスコア変化の追跡
5RUM データ実ユーザーのパフォーマンス測定

これらの体制が整っていないと、パフォーマンス劣化に気づくのが遅れ、ユーザー体験の悪化やコンバージョン率の低下を招いてしまいます。

ビルド設定の複雑さ

Preact の最適化には、ビルドツール(Vite、Webpack など)の深い理解が求められます。適切な設定を行わないと、以下のような問題が発生しますね。

デッドコードが残留してしまい、tree-shaking が機能しないことがあります。また、チャンク分割が適切でないと、キャッシュ効率が悪化し、変更のたびにすべてのコードを再ダウンロードさせることになります。

さらに、開発環境と本番環境で設定が異なるため、開発時には気づかない問題が本番で発生することもあるのです。この乖離を最小化するための設定管理も重要な課題となります。

解決策

ビルド設定の最適化

Lighthouse 95 点超えを実現するためには、ビルド設定の徹底的な最適化が不可欠です。ここでは Vite を使用した実践的な設定例をご紹介します。

Vite の基本設定

まず、Vite の設定ファイルを作成しましょう。この設定では、バンドルサイズの最小化と効率的なコード分割を実現します。

typescript// vite.config.ts の基本設定
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';

次に、ビルド最適化のための詳細設定を追加します。

typescript// ビルド最適化設定
export default defineConfig({
  plugins: [preact()],

  build: {
    // 本番環境での最適化を有効化
    minify: 'terser',

    // Terser の詳細オプション
    terserOptions: {
      compress: {
        drop_console: true, // console.log を削除
        drop_debugger: true, // debugger 文を削除
        pure_funcs: ['console.log', 'console.info'], // 特定の関数を削除
      },
    },

続いて、チャンク分割の設定を行います。これにより、効率的なキャッシュ戦略が実現できます。

typescript    // チャンク分割の最適化
    rollupOptions: {
      output: {
        manualChunks: {
          // Preact 本体を独立したチャンクに
          'preact-core': ['preact', 'preact/hooks'],

          // ルーティングライブラリを分離
          'router': ['preact-router'],

          // 大きなライブラリを個別チャンクに
          'vendor': ['date-fns', 'axios'],
        },

さらに、ファイル名にハッシュを含めることで、長期キャッシュを実現します。

typescript        // ファイル名にハッシュを含める(キャッシュ最適化)
        chunkFileNames: 'assets/[name]-[hash].js',
        entryFileNames: 'assets/[name]-[hash].js',
        assetFileNames: 'assets/[name]-[hash].[ext]',
      },
    },

バンドルサイズの上限を設定し、意図しない肥大化を防ぎます。

typescript    // バンドルサイズの警告閾値
    chunkSizeWarningLimit: 500, // 500KB を超えたら警告

    // レガシーブラウザ対応を無効化(モダンブラウザのみ対応)
    target: 'es2015',
  },
});

この設定により、Preact アプリケーションのバンドルサイズを最小化し、効率的なキャッシュ戦略を実現できます。

依存関係の最適化

次に、依存関係を最適化するための設定を追加しましょう。これにより、不要なコードの除去と高速な起動を実現します。

typescript// vite.config.ts に追加する依存関係最適化設定
export default defineConfig({
  // ... 前述の設定 ...

  optimizeDeps: {
    // 事前バンドル対象を明示
    include: [
      'preact',
      'preact/hooks',
      'preact-router',
    ],

tree-shaking を確実に機能させるための設定を追加します。

typescript    // tree-shaking を確実に機能させる
    esbuildOptions: {
      treeShaking: true,

      // 不要なコードを削除
      pure: ['console.log', 'console.info'],

      // ターゲットブラウザ設定
      target: 'es2015',
    },
  },
});

以下の図は、最適化されたビルドプロセスの流れを示しています。

mermaidflowchart LR
  src["ソースコード"]
  analyze["依存関係解析"]
  treeshake["Tree Shaking"]
  split["コード分割"]
  minify["Minify/Terser"]
  output["最適化バンドル"]

  src --> analyze
  analyze --> treeshake
  treeshake --> split
  split --> minify
  minify --> output

  style treeshake fill:#c8e6c9
  style split fill:#fff9c4
  style minify fill:#ffccbc

この図から分かるように、複数の最適化ステップを経ることで、最終的なバンドルサイズを劇的に削減できます。

画像最適化の実装

画像は Web ページのサイズの大部分を占めるため、最適化が重要です。次世代画像フォーマットの使用と遅延読み込みを実装しましょう。

画像最適化プラグインの導入

まず、Vite で画像を自動最適化するプラグインを導入します。

typescript// vite.config.ts に画像最適化プラグインを追加
import { defineConfig } from 'vite';
import preact from '@preact/preset-vite';
import imagemin from 'vite-plugin-imagemin';

画像最適化の詳細設定を行います。WebP と AVIF フォーマットへの変換を含めます。

typescriptexport default defineConfig({
  plugins: [
    preact(),

    // 画像最適化プラグイン
    imagemin({
      // PNG の最適化
      optipng: {
        optimizationLevel: 7,
      },

      // JPEG の最適化
      mozjpeg: {
        quality: 80,
      },

      // WebP への変換
      webp: {
        quality: 80,
      },

      // SVG の最適化
      svgo: {
        plugins: [
          { name: 'removeViewBox', active: false },
          { name: 'removeEmptyAttrs', active: true },
        ],
      },
    }),
  ],
});

レスポンシブ画像コンポーネント

次に、適切なサイズの画像を配信するためのコンポーネントを作成します。

typescript// components/OptimizedImage.tsx
import { h } from 'preact';

// 画像プロパティの型定義
interface OptimizedImageProps {
  src: string; // 画像のベース URL
  alt: string; // 代替テキスト
  width?: number; // 表示幅
  height?: number; // 表示高さ
  loading?: 'lazy' | 'eager'; // 読み込み方式
}

次に、複数サイズの画像 URL を生成するヘルパー関数を実装します。

typescript// レスポンシブ画像コンポーネントの実装
export function OptimizedImage({
  src,
  alt,
  width,
  height,
  loading = 'lazy',
}: OptimizedImageProps) {
  // 複数サイズの画像 URL を生成
  const srcSet = [
    `${src}?w=320&format=webp 320w`,
    `${src}?w=640&format=webp 640w`,
    `${src}?w=1024&format=webp 1024w`,
    `${src}?w=1920&format=webp 1920w`,
  ].join(', ');

最後に、picture 要素を使用して次世代フォーマットと従来フォーマットの両方に対応します。

typescript  return (
    <picture>
      {/* WebP フォーマット(モダンブラウザ用) */}
      <source
        type="image/webp"
        srcSet={srcSet}
        sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
      />

      {/* フォールバック(レガシーブラウザ用) */}
      <img
        src={`${src}?w=1024`}
        alt={alt}
        width={width}
        height={height}
        loading={loading}
        decoding="async"
      />
    </picture>
  );
}

このコンポーネントを使用することで、デバイスに応じた最適なサイズの画像が自動的に配信され、LCP の大幅な改善が期待できます。

コード分割とプリロード戦略

効率的なコード分割により、初回ロード時間を短縮し、TTI を改善できます。

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

まず、Preact Router を使用したルートベースのコード分割を実装します。

typescript// App.tsx - ルートベースのコード分割
import { h } from 'preact';
import { Router } from 'preact-router';
import { lazy, Suspense } from 'preact/compat';

// 各ページコンポーネントを遅延ロード
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Dashboard = lazy(() => import('./pages/Dashboard'));

ローディング状態を表示するコンポーネントを作成します。

typescript// ローディングコンポーネント
function LoadingSpinner() {
  return (
    <div class='loading-spinner'>
      <div class='spinner' />
      <p>読み込み中...</p>
    </div>
  );
}

Router と Suspense を組み合わせて、コード分割を実装します。

typescript// アプリケーションのメインコンポーネント
export function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Router>
        {/* 各ルートは必要になるまでロードされない */}
        <Home path='/' />
        <About path='/about' />
        <Dashboard path='/dashboard' />
      </Router>
    </Suspense>
  );
}

プリロード戦略の実装

ユーザーがリンクにホバーした際に、次のページを事前にロードする仕組みを実装しましょう。

typescript// hooks/usePrefetch.ts
import { useEffect } from 'preact/hooks';

// プリフェッチ関数の型定義
type PrefetchFunction = () => Promise<any>;

// プリフェッチフック
export function usePrefetch(
  prefetchFn: PrefetchFunction,
  shouldPrefetch: boolean = true
) {
  useEffect(() => {
    if (!shouldPrefetch) return;

    // アイドル時にプリフェッチを実行
    if ('requestIdleCallback' in window) {
      requestIdleCallback(() => {
        prefetchFn();
      });
    } else {
      // フォールバック(Safari など)
      setTimeout(() => {
        prefetchFn();
      }, 1000);
    }
  }, [shouldPrefetch]);
}

次に、リンクコンポーネントにプリフェッチ機能を組み込みます。

typescript// components/PrefetchLink.tsx
import { h } from 'preact';
import { Link } from 'preact-router';
import { useState } from 'preact/hooks';

interface PrefetchLinkProps {
  href: string;
  children: preact.ComponentChildren;
  prefetch: () => Promise<any>;
}

// プリフェッチ機能付きリンク
export function PrefetchLink({
  href,
  children,
  prefetch,
}: PrefetchLinkProps) {
  const [prefetched, setPrefetched] = useState(false);

  // ホバー時にプリフェッチ
  const handleMouseEnter = () => {
    if (!prefetched) {
      prefetch();
      setPrefetched(true);
    }
  };

  return (
    <Link href={href} onMouseEnter={handleMouseEnter}>
      {children}
    </Link>
  );
}

このプリフェッチ戦略により、ユーザーがリンクをクリックする前にコードのダウンロードが完了し、スムーズな画面遷移が実現できます。

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

継続的にパフォーマンスを監視するために、具体的な KPI を設定しましょう。

監視すべき Core Web Vitals

Google が定義する Core Web Vitals は、ユーザー体験を測る重要な指標です。以下の閾値を目標としましょう。

#指標略称良好改善が必要不良
1Largest Contentful PaintLCP2.5s 以下2.5s〜4.0s4.0s 超
2First Input DelayFID100ms 以下100ms〜300ms300ms 超
3Cumulative Layout ShiftCLS0.1 以下0.1〜0.250.25 超
4First Contentful PaintFCP1.8s 以下1.8s〜3.0s3.0s 超
5Time to InteractiveTTI3.8s 以下3.8s〜7.3s7.3s 超

これらの指標を継続的に監視し、「良好」の範囲内に維持することが目標となります。

Lighthouse CI の導入

CI/CD パイプラインに Lighthouse を組み込み、デプロイ前にスコアをチェックする仕組みを構築しましょう。

まず、Lighthouse CI の設定ファイルを作成します。

javascript// lighthouserc.js - Lighthouse CI 設定
module.exports = {
  ci: {
    collect: {
      // 測定対象のURL
      url: [
        'http://localhost:3000/',
        'http://localhost:3000/about',
        'http://localhost:3000/dashboard',
      ],

      // 測定回数(中央値を取得)
      numberOfRuns: 3,

続いて、アップロード先とアサーション(閾値)の設定を行います。

javascript      // ビルド済みファイルのディレクトリ
      startServerCommand: 'yarn preview',
      startServerReadyPattern: 'Local:',
    },

    upload: {
      // 結果の保存先(Lighthouse Server または一時ストレージ)
      target: 'temporary-public-storage',
    },

最後に、許容できるスコアの下限値を設定します。この閾値を下回るとビルドが失敗します。

javascript    assert: {
      // 閾値設定(これを下回るとビルド失敗)
      assertions: {
        'categories:performance': ['error', { minScore: 0.95 }],  // 95点以上
        'categories:accessibility': ['warn', { minScore: 0.90 }], // 90点以上
        'categories:best-practices': ['warn', { minScore: 0.95 }],
        'categories:seo': ['warn', { minScore: 0.90 }],

        // Core Web Vitals の閾値
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'total-blocking-time': ['error', { maxNumericValue: 300 }],
      },
    },
  },
};

GitHub Actions への統合

次に、GitHub Actions で自動的に Lighthouse CI を実行する設定を作成します。

yaml# .github/workflows/lighthouse.yml
name: Lighthouse CI

on:
  # プルリクエスト時に実行
  pull_request:
    branches: [main, develop]

  # 手動実行も可能
  workflow_dispatch:

jobs:
  lighthouse:
    runs-on: ubuntu-latest

    steps:
      # リポジトリのチェックアウト
      - name: Checkout code
        uses: actions/checkout@v3

依存関係のインストールとビルドを実行します。

yaml# Node.js のセットアップ
- name: Setup Node.js
  uses: actions/setup-node@v3
  with:
    node-version: '18'
    cache: 'yarn'

# 依存関係のインストール
- name: Install dependencies
  run: yarn install --frozen-lockfile

# プロダクションビルド
- name: Build application
  run: yarn build

Lighthouse CI を実行し、結果をコメントとして投稿します。

yaml# Lighthouse CI の実行
- name: Run Lighthouse CI
  run: |
    yarn global add @lhci/cli
    lhci autorun
  env:
    LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

# 結果をプルリクエストにコメント
- name: Comment results
  uses: actions/github-script@v6
  if: github.event_name == 'pull_request'
  with:
    script: |
      // Lighthouse の結果を取得してコメント
      const fs = require('fs');
      const results = JSON.parse(
        fs.readFileSync('.lighthouseci/manifest.json', 'utf8')
      );
      // コメント投稿処理(省略)

この設定により、プルリクエストごとに自動的にパフォーマンスがチェックされ、基準を満たさないコードはマージできなくなります。

リアルユーザーモニタリング(RUM)の実装

Lighthouse はラボデータ(シミュレーション環境)での測定ですが、実際のユーザー環境でのパフォーマンスも監視する必要があります。

Web Vitals ライブラリの導入

まず、Google の web-vitals ライブラリをインストールします。

bash# Web Vitals ライブラリのインストール
yarn add web-vitals

次に、パフォーマンスデータを収集する関数を実装します。

typescript// utils/analytics.ts
import {
  getCLS,
  getFID,
  getFCP,
  getLCP,
  getTTFB,
} from 'web-vitals';

// パフォーマンスデータの型定義
interface PerformanceMetric {
  name: string; // 指標名
  value: number; // 測定値
  rating: string; // 評価(good/needs-improvement/poor)
  delta: number; // 前回からの変化量
  id: string; // 測定ID
}

各指標を収集し、分析サーバーに送信する関数を実装します。

typescript// パフォーマンスデータの送信関数
function sendToAnalytics(metric: PerformanceMetric) {
  // 分析サーバーに送信(例:Google Analytics、独自サーバーなど)
  const body = JSON.stringify({
    metric: metric.name,
    value: Math.round(metric.value),
    rating: metric.rating,
    page: window.location.pathname,
    timestamp: Date.now(),
  });

  // Beacon API で送信(ページ離脱時も確実に送信)
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/analytics', body);
  } else {
    // フォールバック
    fetch('/api/analytics', {
      method: 'POST',
      body,
      keepalive: true,
    });
  }
}

すべての Core Web Vitals を測定する初期化関数を作成します。

typescript// パフォーマンス測定の初期化
export function initPerformanceMonitoring() {
  // 各 Web Vitals を測定
  getCLS(sendToAnalytics); // Cumulative Layout Shift
  getFID(sendToAnalytics); // First Input Delay
  getFCP(sendToAnalytics); // First Contentful Paint
  getLCP(sendToAnalytics); // Largest Contentful Paint
  getTTFB(sendToAnalytics); // Time to First Byte
}

最後に、アプリケーションの起動時に監視を開始します。

typescript// main.tsx または index.tsx で初期化
import { render } from 'preact';
import { App } from './App';
import { initPerformanceMonitoring } from './utils/analytics';

// アプリケーションのレンダリング
render(<App />, document.getElementById('app')!);

// パフォーマンス監視の開始
if (import.meta.env.PROD) {
  // 本番環境のみで実行
  initPerformanceMonitoring();
}

ダッシュボードの構築

収集したデータを可視化するダッシュボードを構築しましょう。以下のような構成が推奨されます。

mermaidflowchart LR
  browser["ブラウザ<br/>(Web Vitals 測定)"]
  beacon["Beacon API"]
  api["分析 API<br/>(Node.js)"]
  db[("時系列 DB<br/>(InfluxDB/TimescaleDB)")]
  dashboard["ダッシュボード<br/>(Grafana)"]

  browser -->|送信| beacon
  beacon -->|保存| api
  api -->|書き込み| db
  db -->|可視化| dashboard

  style browser fill:#e3f2fd
  style db fill:#fff3e0
  style dashboard fill:#f3e5f5

この構成により、リアルタイムでユーザーのパフォーマンスデータを収集・分析し、問題を早期に検知できます。

具体例

実際のプロジェクトでの最適化事例

ここでは、EC サイトを Preact で構築した際の具体的な最適化プロセスをご紹介します。

最適化前の状態

最適化前の Lighthouse スコアは以下の通りでした。

#カテゴリスコア主な問題点
1Performance72 点バンドルサイズ 850KB、LCP 4.2s
2Accessibility88 点色のコントラスト不足
3Best Practices83 点HTTP/2 未使用
4SEO92 点meta description なし

特に Performance スコアが低く、ユーザー体験に悪影響を及ぼしていました。

ステップ 1:バンドルサイズの分析

まず、バンドルアナライザーを使用して、どのライブラリがサイズを圧迫しているかを調査しました。

bash# Rollup Plugin Visualizer のインストール
yarn add -D rollup-plugin-visualizer

Vite 設定にバンドルアナライザーを追加します。

typescript// vite.config.ts にバンドルアナライザーを追加
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    // ... 他のプラグイン ...

    // バンドルアナライザー
    visualizer({
      open: true, // ビルド後に自動で開く
      filename: 'stats.html', // 出力ファイル名
      gzipSize: true, // gzip 圧縮後のサイズも表示
      brotliSize: true, // brotli 圧縮後のサイズも表示
    }),
  ],
});

分析の結果、以下の問題が判明しました。

typescript// 問題のあったインポート例
import moment from 'moment'; // 288KB(不要なロケールも含む)
import _ from 'lodash'; // 69KB(使用は 3 関数のみ)
import { Chart } from 'chart.js'; // 234KB(基本機能のみ使用)

ステップ 2:依存関係の置き換え

大きなライブラリを軽量な代替品に置き換えました。

typescript// 最適化後のインポート
import { format, parseISO } from 'date-fns'; // 11KB(必要な関数のみ)
import debounce from 'lodash-es/debounce'; // 2KB(関数単位でインポート)
import { Chart } from 'chart.js/auto'; // 87KB(tree-shaking 対応版)

date-fns を使用した日付フォーマット関数の実装例です。

typescript// utils/dateFormat.ts
import { format, parseISO } from 'date-fns';
import { ja } from 'date-fns/locale';

/**
 * ISO 8601 形式の日付文字列を日本語フォーマットに変換
 * @param isoString - ISO 8601 形式の日付文字列
 * @returns フォーマット済み日付文字列
 */
export function formatDate(isoString: string): string {
  const date = parseISO(isoString);
  return format(date, 'yyyy年MM月dd日', { locale: ja });
}

この変更により、バンドルサイズが 850KB から 420KB に削減されました(約 50% の削減)。

ステップ 3:画像の最適化

商品画像をすべて WebP フォーマットに変換し、レスポンシブ対応を実装しました。

typescript// components/ProductImage.tsx
import { h } from 'preact';
import { OptimizedImage } from './OptimizedImage';

interface ProductImageProps {
  productId: string;
  alt: string;
  priority?: boolean; // 優先度の高い画像(LCP 対象)
}

export function ProductImage({
  productId,
  alt,
  priority = false,
}: ProductImageProps) {
  // 画像 URL の生成(CDN + 変換パラメータ)
  const baseUrl = `https://cdn.example.com/products/${productId}`;

  return (
    <OptimizedImage
      src={baseUrl}
      alt={alt}
      loading={priority ? 'eager' : 'lazy'}
      width={800}
      height={600}
    />
  );
}

また、ファーストビューの画像にはプリロードを設定しました。

typescript// components/Head.tsx(head 要素内に配置)
export function PreloadCriticalImages() {
  return (
    <>
      {/* LCP 対象の画像をプリロード */}
      <link
        rel='preload'
        as='image'
        href='/hero-image.webp'
        type='image/webp'
      />

      {/* 次に重要な画像 */}
      <link
        rel='preload'
        as='image'
        href='/featured-product.webp'
        type='image/webp'
      />
    </>
  );
}

この最適化により、LCP が 4.2s から 1.8s に短縮されました(約 57% の改善)。

ステップ 4:コード分割の徹底

管理画面など、一部のユーザーしかアクセスしない機能を完全に分離しました。

typescript// App.tsx - コード分割の実装
import { h, lazy, Suspense } from 'preact/compat';
import { Router } from 'preact-router';

// パブリックページ(通常のインポート)
import { Home } from './pages/Home';
import { ProductList } from './pages/ProductList';

// 管理画面(遅延ロード)
const AdminDashboard = lazy(
  () => import('./pages/admin/Dashboard')
);
const AdminProducts = lazy(
  () => import('./pages/admin/Products')
);
const AdminOrders = lazy(
  () => import('./pages/admin/Orders')
);

// 重いライブラリを含むページ(遅延ロード)
const Analytics = lazy(() => import('./pages/Analytics'));

ルーティング設定でコード分割を適用します。

typescriptexport function App() {
  return (
    <Router>
      {/* 通常ページ(即座にロード) */}
      <Home path='/' />
      <ProductList path='/products' />

      {/* 管理画面(必要時にロード) */}
      <Suspense fallback={<LoadingSpinner />}>
        <AdminDashboard path='/admin' />
        <AdminProducts path='/admin/products' />
        <AdminOrders path='/admin/orders' />
        <Analytics path='/admin/analytics' />
      </Suspense>
    </Router>
  );
}

この変更により、初回ロード時のバンドルサイズがさらに 420KB から 180KB に削減され、TTI が 5.1s から 2.3s に改善されました。

ステップ 5:フォントの最適化

Web フォントの読み込みも最適化対象です。

html<!-- index.html - フォントの最適化 -->
<head>
  <!-- DNS プリコネクト -->
  <link
    rel="preconnect"
    href="https://fonts.googleapis.com"
  />
  <link
    rel="preconnect"
    href="https://fonts.gstatic.com"
    crossorigin
  />

  <!-- フォントのプリロード(最重要フォントのみ) -->
  <link
    rel="preload"
    as="font"
    type="font/woff2"
    href="https://fonts.gstatic.com/s/notosansjp/v42/NotoSansJP-Regular.woff2"
    crossorigin
  />

  <!-- フォントの読み込み(display=swap でテキスト表示を優先) -->
  <link
    href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;700&display=swap"
    rel="stylesheet"
  />
</head>

CSS でフォント読み込み戦略を設定します。

css/* styles/fonts.css */
@font-face {
  font-family: 'Noto Sans JP';
  font-style: normal;
  font-weight: 400;
  /* swap: フォントロード中はシステムフォントで表示 */
  font-display: swap;
  src: url('https://fonts.gstatic.com/s/notosansjp/v42/NotoSansJP-Regular.woff2')
    format('woff2');
}

/* フォールバックフォントスタック */
body {
  font-family: 'Noto Sans JP', -apple-system, BlinkMacSystemFont,
    'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
}

最適化後の結果

すべての最適化を適用した結果、Lighthouse スコアは以下のように改善されました。

#カテゴリ最適化前最適化後改善
1Performance72 点97 点+25 点
2Accessibility88 点95 点+7 点
3Best Practices83 点100 点+17 点
4SEO92 点100 点+8 点

Core Web Vitals の改善結果も顕著でした。

#指標最適化前最適化後改善率
1LCP4.2s1.8s57% 改善
2FID180ms45ms75% 改善
3CLS0.180.0572% 改善
4TTI5.1s2.3s55% 改善

これらの改善により、コンバージョン率が 12% 向上し、直帰率が 8% 低下するという、ビジネス成果にも直結する結果が得られました。

継続的な監視体制の構築

最適化は一度行えば終わりではなく、継続的な監視が必要です。実際に構築した監視体制をご紹介します。

監視ダッシュボードの設定

Grafana を使用してパフォーマンスダッシュボードを構築しました。以下の指標を可視化しています。

typescript// grafana/dashboard.json の抜粋(設定例)
{
  "dashboard": {
    "title": "Preact App Performance Monitoring",
    "panels": [
      {
        "title": "Core Web Vitals - LCP",
        "targets": [{
          "query": "SELECT mean(value) FROM lcp WHERE time > now() - 7d GROUP BY time(1h)"
        }],
        "thresholds": [
          { "value": 2500, "color": "green" },
          { "value": 4000, "color": "yellow" },
          { "value": 4001, "color": "red" }
        ]
      }
    ]
  }
}

アラート設定

パフォーマンスが閾値を下回った場合、Slack に通知する設定を行いました。

typescript// monitoring/alerts.ts
interface AlertRule {
  metric: string;
  threshold: number;
  duration: string;
  severity: 'warning' | 'critical';
}

// アラートルールの定義
const alertRules: AlertRule[] = [
  {
    metric: 'lcp',
    threshold: 2500,
    duration: '5m',
    severity: 'warning',
  },
  {
    metric: 'lcp',
    threshold: 4000,
    duration: '5m',
    severity: 'critical',
  },
  {
    metric: 'cls',
    threshold: 0.1,
    duration: '5m',
    severity: 'warning',
  },
  {
    metric: 'fid',
    threshold: 100,
    duration: '5m',
    severity: 'warning',
  },
];

Slack への通知処理を実装します。

typescript// Slack 通知関数
async function sendSlackAlert(
  metric: string,
  value: number,
  threshold: number,
  severity: 'warning' | 'critical'
) {
  const color =
    severity === 'critical' ? 'danger' : 'warning';
  const emoji =
    severity === 'critical'
      ? ':rotating_light:'
      : ':warning:';

  await fetch(process.env.SLACK_WEBHOOK_URL!, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      attachments: [
        {
          color,
          title: `${emoji} パフォーマンスアラート`,
          fields: [
            {
              title: '指標',
              value: metric.toUpperCase(),
              short: true,
            },
            {
              title: '現在値',
              value: `${value}ms`,
              short: true,
            },
            {
              title: '閾値',
              value: `${threshold}ms`,
              short: true,
            },
            {
              title: '重要度',
              value: severity,
              short: true,
            },
          ],
          footer: 'Performance Monitoring',
          ts: Math.floor(Date.now() / 1000),
        },
      ],
    }),
  });
}

以下の図は、監視とアラートのフローを示しています。

mermaidflowchart TD
  rum["RUM データ収集"]
  storage["時系列 DB"]
  analysis["分析処理"]
  check{"閾値チェック"}
  alert["アラート発火"]
  slack["Slack 通知"]
  dashboard["ダッシュボード更新"]

  rum -->|保存| storage
  storage -->|定期実行| analysis
  analysis --> check
  check -->|異常| alert
  check -->|正常| dashboard
  alert --> slack
  alert --> dashboard

  style check fill:#fff9c4
  style alert fill:#ffcdd2
  style dashboard fill:#c8e6c9

この監視体制により、パフォーマンス劣化を即座に検知し、迅速な対応が可能になります。

週次レポートの自動生成

週次でパフォーマンスレポートを自動生成し、チームで共有する仕組みも構築しました。

typescript// scripts/generate-weekly-report.ts
import { getWeeklyMetrics } from './db';
import { generateChart } from './charts';
import { sendEmail } from './email';

interface WeeklyMetrics {
  lcp: { average: number; p75: number; p95: number };
  fid: { average: number; p75: number; p95: number };
  cls: { average: number; p75: number; p95: number };
}

// 週次レポートの生成
async function generateWeeklyReport() {
  // 過去 1 週間のメトリクスを取得
  const metrics: WeeklyMetrics = await getWeeklyMetrics();

  // チャートを生成
  const lcpChart = await generateChart('lcp', metrics.lcp);
  const fidChart = await generateChart('fid', metrics.fid);
  const clsChart = await generateChart('cls', metrics.cls);

レポートの内容を HTML で生成します。

typescript// レポート本文の生成
const reportHtml = `
    <h1>週次パフォーマンスレポート</h1>

    <h2>サマリー</h2>
    <table>
      <tr>
        <th>指標</th>
        <th>平均値</th>
        <th>75パーセンタイル</th>
        <th>95パーセンタイル</th>
        <th>評価</th>
      </tr>
      <tr>
        <td>LCP</td>
        <td>${metrics.lcp.average}ms</td>
        <td>${metrics.lcp.p75}ms</td>
        <td>${metrics.lcp.p95}ms</td>
        <td>${
          metrics.lcp.p75 < 2500 ? '✅ 良好' : '⚠️ 要改善'
        }</td>
      </tr>
      <!-- FID, CLS も同様 -->
    </table>

    <h2>トレンドチャート</h2>
    <img src="${lcpChart}" alt="LCP トレンド" />
    <img src="${fidChart}" alt="FID トレンド" />
    <img src="${clsChart}" alt="CLS トレンド" />
  `;

生成したレポートをメールで送信します。

typescript  // メール送信
  await sendEmail({
    to: 'team@example.com',
    subject: `週次パフォーマンスレポート - ${new Date().toLocaleDateString('ja-JP')}`,
    html: reportHtml,
  });
}

// 毎週月曜日 9:00 に実行(cron)
export default generateWeeklyReport;

このレポートにより、チーム全体でパフォーマンスの状況を共有し、継続的な改善につなげることができます。

まとめ

Preact で Lighthouse 95 点超えを達成し、維持するためには、適切なビルド設定と継続的な監視体制の両方が不可欠です。

ビルド設定の最適化では、Vite や Webpack の詳細な設定により、バンドルサイズを最小化し、効率的なコード分割を実現しましたね。特に、依存関係の見直しと tree-shaking の徹底、画像の次世代フォーマット対応が大きな効果を生みました。

監視体制の構築では、Lighthouse CI を CI/CD パイプラインに組み込み、デプロイ前にパフォーマンスをチェックする仕組みを整えました。また、リアルユーザーモニタリング(RUM)により、実際のユーザー環境でのパフォーマンスを継続的に測定し、問題の早期発見を可能にしました。

具体的な KPI としては、Core Web Vitals(LCP 2.5s 以下、FID 100ms 以下、CLS 0.1 以下)を目標に設定し、これらを継続的に監視することで、ユーザー体験の質を保証できます。

パフォーマンス最適化は一度行えば終わりではなく、継続的な取り組みが求められますが、本記事でご紹介した手法を実践することで、Lighthouse 95 点超えを安定して維持できるでしょう。これにより、優れたユーザー体験の提供とビジネス成果の向上の両立が実現します。

関連リンク