Preact 本番最適化運用:Lighthouse 95 点超えのビルド設定と監視 KPI
Web アプリケーションのパフォーマンスは、ユーザー体験やビジネス成果に直結する重要な要素です。特に Preact のような軽量フレームワークを採用する際には、その利点を最大限に活かした最適化が求められます。
本記事では、Lighthouse で 95 点以上を安定して維持するための Preact 本番環境の最適化手法を、実践的なビルド設定と継続的な監視 KPI の設定方法を中心に解説します。実際の運用で効果が実証された具体的な設定例と、パフォーマンス劣化を早期に検知するための監視指標をご紹介しますので、すぐに実務に活かしていただけるでしょう。
背景
Preact を選択する理由
Preact は React の軽量代替フレームワークとして、わずか 3KB のサイズで React とほぼ同じ API を提供します。モバイルファーストの時代において、初期ロード時間の短縮は SEO やコンバージョン率に大きく影響するため、多くのプロジェクトで採用が進んでいますね。
しかし、フレームワークが軽量であっても、適切なビルド設定や運用体制がなければ、その恩恵を十分に受けることはできません。依存ライブラリの増加、不適切なコード分割、画像の最適化不足などにより、気づかないうちにバンドルサイズが肥大化することがあります。
Lighthouse スコアの重要性
Lighthouse は Google が提供するパフォーマンス監査ツールで、以下の 5 つの指標で Web サイトを評価します。
| # | カテゴリ | 説明 |
|---|---|---|
| 1 | Performance | ページの読み込み速度と応答性 |
| 2 | Accessibility | アクセシビリティへの対応度 |
| 3 | Best Practices | セキュリティやモダンな実装の適用度 |
| 4 | SEO | 検索エンジン最適化の評価 |
| 5 | PWA | プログレッシブウェブアプリの対応度 |
特に 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 | トレンド分析 | 時系列でのスコア変化の追跡 |
| 5 | RUM データ | 実ユーザーのパフォーマンス測定 |
これらの体制が整っていないと、パフォーマンス劣化に気づくのが遅れ、ユーザー体験の悪化やコンバージョン率の低下を招いてしまいます。
ビルド設定の複雑さ
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 は、ユーザー体験を測る重要な指標です。以下の閾値を目標としましょう。
| # | 指標 | 略称 | 良好 | 改善が必要 | 不良 |
|---|---|---|---|---|---|
| 1 | Largest Contentful Paint | LCP | 2.5s 以下 | 2.5s〜4.0s | 4.0s 超 |
| 2 | First Input Delay | FID | 100ms 以下 | 100ms〜300ms | 300ms 超 |
| 3 | Cumulative Layout Shift | CLS | 0.1 以下 | 0.1〜0.25 | 0.25 超 |
| 4 | First Contentful Paint | FCP | 1.8s 以下 | 1.8s〜3.0s | 3.0s 超 |
| 5 | Time to Interactive | TTI | 3.8s 以下 | 3.8s〜7.3s | 7.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 スコアは以下の通りでした。
| # | カテゴリ | スコア | 主な問題点 |
|---|---|---|---|
| 1 | Performance | 72 点 | バンドルサイズ 850KB、LCP 4.2s |
| 2 | Accessibility | 88 点 | 色のコントラスト不足 |
| 3 | Best Practices | 83 点 | HTTP/2 未使用 |
| 4 | SEO | 92 点 | 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 スコアは以下のように改善されました。
| # | カテゴリ | 最適化前 | 最適化後 | 改善 |
|---|---|---|---|---|
| 1 | Performance | 72 点 | 97 点 | +25 点 |
| 2 | Accessibility | 88 点 | 95 点 | +7 点 |
| 3 | Best Practices | 83 点 | 100 点 | +17 点 |
| 4 | SEO | 92 点 | 100 点 | +8 点 |
Core Web Vitals の改善結果も顕著でした。
| # | 指標 | 最適化前 | 最適化後 | 改善率 |
|---|---|---|---|---|
| 1 | LCP | 4.2s | 1.8s | 57% 改善 |
| 2 | FID | 180ms | 45ms | 75% 改善 |
| 3 | CLS | 0.18 | 0.05 | 72% 改善 |
| 4 | TTI | 5.1s | 2.3s | 55% 改善 |
これらの改善により、コンバージョン率が 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 点超えを安定して維持できるでしょう。これにより、優れたユーザー体験の提供とビジネス成果の向上の両立が実現します。
関連リンク
articlePreact 本番最適化運用:Lighthouse 95 点超えのビルド設定と監視 KPI
articlePreact で Hydration mismatch が出る原因と完全解決チェックリスト
articlePreact で埋め込みウィジェット配布:他サイトに設置できる軽量 UI の作り方
articlePreact でミニブログを 1 日で公開:ルーティング・MDX・SEO まで一気通貫
articlePreact でスケーラブルな状態管理:Signals/Context/外部ストアの責務分離
articlePreact チートシート【保存版】:JSX/Props/Events/Ref の書き方早見表
articleNuxt × Vercel/Netlify/Cloudflare:デプロイ先で変わる性能とコストを実測
articleRemix で「Hydration failed」を解決:サーバ/クライアント不整合の診断手順
articlePreact 本番最適化運用:Lighthouse 95 点超えのビルド設定と監視 KPI
articleNginx microcaching vs 上流キャッシュ(Varnish/Redis)比較:TTFB と整合性の最適解
articleNestJS × TypeORM vs Prisma vs Drizzle:DX・性能・移行性の総合ベンチ
articlePlaywright × Allure レポート運用:履歴・トレンド・失敗分析を見える化する
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来