LESSON 30分

ストーリー

高橋アーキテクト
bundle.jsが2.5MBある。これではモバイルユーザーの初回表示に8秒以上かかる

高橋アーキテクトがバンドル分析ツールの出力を見せた。

高橋アーキテクト
2.5MBのうち、初期表示に本当に必要なのは300KB程度だ。残りは使われるかどうかも分からない。Tree-shaking、Code-splitting、Lazy loadingで劇的に改善できる

Tree-shaking

使用されていないコードをバンドルから自動的に除去する最適化です。

// utils.ts - 多数のユーティリティ関数
export function formatDate(date: Date): string { /* ... */ }
export function formatCurrency(amount: number): string { /* ... */ }
export function formatPhone(phone: string): string { /* ... */ }
export function formatAddress(addr: Address): string { /* ... */ }

// page.ts - formatDate だけ使用
import { formatDate } from './utils';
// Tree-shaking により、formatCurrency, formatPhone, formatAddress はバンドルに含まれない

Tree-shakingを阻害するパターン

// NG: 名前空間インポート → 全関数がバンドルに含まれる
import * as utils from './utils';
utils.formatDate(new Date());

// NG: CommonJS モジュール(require)→ 静的解析不可
const utils = require('./utils');

// OK: 名前付きインポート → 使用関数のみバンドル
import { formatDate } from './utils';

// OK: ES Modules → 静的解析可能
export { formatDate } from './utils';

Code-splitting(コード分割)

バンドルを複数のチャンクに分割し、必要なタイミングで読み込みます。

// Before: 全てを1つのバンドルに
import { Dashboard } from './pages/Dashboard';
import { Settings } from './pages/Settings';
import { Analytics } from './pages/Analytics';
import { AdminPanel } from './pages/AdminPanel'; // 管理者のみが使う

// After: ルートベースのCode-splitting(React例)
import { lazy, Suspense } from 'react';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));
const AdminPanel = lazy(() => import('./pages/AdminPanel'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
        <Route path="/analytics" element={<Analytics />} />
        <Route path="/admin" element={<AdminPanel />} />
      </Routes>
    </Suspense>
  );
}

分割戦略

戦略説明効果
ルートベースページごとに分割初期ロードサイズを大幅削減
コンポーネントベース重いUIコンポーネントを分割モーダル、チャートなど
ベンダー分割サードパーティライブラリを分離キャッシュ効率の向上

Lazy Loading(遅延読み込み)

表示領域に入った時点で初めてリソースを読み込みます。

// 画像の遅延読み込み
// <img loading="lazy" src="product.jpg" alt="商品画像">

// Intersection Observer を使った遅延読み込み
class LazyLoader {
  private observer: IntersectionObserver;

  constructor() {
    this.observer = new IntersectionObserver(
      (entries) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            this.loadElement(entry.target as HTMLElement);
            this.observer.unobserve(entry.target);
          }
        });
      },
      { rootMargin: '200px' } // 200px手前から読み込み開始
    );
  }

  observe(element: HTMLElement): void {
    this.observer.observe(element);
  }

  private loadElement(element: HTMLElement): void {
    const src = element.dataset.src;
    if (src) {
      (element as HTMLImageElement).src = src;
    }
  }
}

// コンポーネントの遅延読み込み(React例)
const HeavyChart = lazy(() => import('./components/HeavyChart'));

function AnalyticsPage() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <h1>分析</h1>
      <button onClick={() => setShowChart(true)}>チャートを表示</button>
      {showChart && (
        <Suspense fallback={<p>チャートを読み込み中...</p>}>
          <HeavyChart />
        </Suspense>
      )}
    </div>
  );
}

バンドル分析

// webpack-bundle-analyzer でバンドルの中身を可視化
// npm install --save-dev webpack-bundle-analyzer

// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html',
    }),
  ],
};

// 確認すべきポイント
const analysisChecklist = [
  '重複しているライブラリはないか',
  '使っていないライブラリが含まれていないか',
  '巨大なライブラリをより軽量な代替に置き換えられないか',
  'ルートごとに適切に分割されているか',
];

// 軽量代替の例
const alternatives = {
  'moment.js (300KB)': 'date-fns (25KB) or dayjs (7KB)',
  'lodash (70KB)': 'lodash-es (Tree-shakable) or 個別インポート',
  'chart.js (200KB)': 'lightweight-charts (40KB)',
};

まとめ

ポイント内容
Tree-shaking未使用コードの自動除去。ES Modulesが前提
Code-splittingバンドルを複数チャンクに分割。ルートベースが基本
Lazy Loading必要になった時点で初めて読み込む
バンドル分析webpack-bundle-analyzerで中身を可視化
目標初期バンドルを200KB以下に抑える

チェックリスト

  • Tree-shakingの仕組みと阻害パターンを理解した
  • Code-splittingの3つの分割戦略を把握した
  • Lazy Loadingの実装方法を理解した
  • バンドル分析ツールの使い方を知った

次のステップへ

次は「画像とアセット最適化」を学びます。Webサイトの転送量の大半を占める画像の最適化テクニックです。


推定読了時間: 30分