T-CREATOR

Storybook の HMR が遅い問題を撃退:大型プロジェクト最適化の実践手順

Storybook の HMR が遅い問題を撃退:大型プロジェクト最適化の実践手順

Storybook を使ったコンポーネント開発は便利ですが、プロジェクトが大きくなるにつれて HMR(Hot Module Replacement)が遅くなり、開発体験が著しく低下することがありますね。

ファイルを保存してから変更が反映されるまで数秒から十数秒かかってしまうと、開発のリズムが崩れてしまいます。 本記事では、大型プロジェクトで実際に効果があった Storybook の HMR 最適化手法を、段階的にご紹介していきます。

背景

Storybook は React や Vue などのコンポーネントを独立した環境で開発・テストするための強力なツールです。 開発中のコンポーネント変更を即座に反映する HMR 機能により、快適な開発体験を提供してくれます。

しかし、プロジェクトの規模が大きくなると、以下のような要因で HMR のパフォーマンスが低下していきます。

HMR が遅くなる主な要因を図で確認してみましょう。

mermaidflowchart TB
  fileChange["ファイル変更"] --> webpack["Webpack/Vite<br/>ビルド処理"]
  webpack --> analysis["依存関係の解析"]
  analysis --> rebuild["モジュール再構築"]
  rebuild --> hmr["HMR 適用"]

  subgraph factors["遅延要因"]
    f1["Story ファイル数<br/>の増加"]
    f2["依存関係の<br/>複雑化"]
    f3["重い addon の<br/>読み込み"]
    f4["TypeScript<br/>の型チェック"]
  end

  factors -.影響.-> webpack
  factors -.影響.-> analysis

図で理解できる要点:

  • ファイル変更から HMR 適用までの処理フローが複数段階に分かれている
  • 複数の要因がビルド処理や依存関係解析に影響を与える
  • Story ファイル数や依存関係が増えるほど処理時間が増大する

プロジェクト規模と HMR 速度の関係

#Story 数コンポーネント数平均 HMR 時間体感
150 以下30 以下1〜2 秒快適
2100〜20050〜1003〜5 秒やや遅い
3300〜500150〜2506〜10 秒遅い
4500 以上250 以上10 秒以上非常に遅い

プロジェクトが成長するにつれて、開発者は変更の反映を待つ時間が増え、生産性が低下してしまうのです。

課題

大型プロジェクトで Storybook の HMR が遅くなる具体的な課題を整理しましょう。

パフォーマンスボトルネックの特定

HMR が遅くなる原因は複数あり、プロジェクトごとに異なります。 主な課題は以下の通りです。

mermaidflowchart LR
  problem["HMR 遅延問題"] --> p1["全 Story の<br/>自動読み込み"]
  problem --> p2["重い addon の<br/>初期化"]
  problem --> p3["型チェックの<br/>実行"]
  problem --> p4["ソースマップ<br/>生成"]

  p1 --> i1["不要な Story も<br/>メモリに展開"]
  p2 --> i2["addon の処理が<br/>ブロック"]
  p3 --> i3["TypeScript が<br/>全ファイル解析"]
  p4 --> i4["デバッグ情報の<br/>生成に時間"]

図で理解できる要点:

  • HMR 遅延は単一の原因ではなく、複数の要因が複合的に影響
  • 各ボトルネックがさらに具体的な問題を引き起こしている
  • 改善には各要因への個別対策が必要

開発体験への影響

HMR の遅延は、開発者の体験に以下のような悪影響を与えます。

待ち時間によるフラストレーション コンポーネントのスタイル調整など、細かい変更を繰り返す作業では、毎回 10 秒も待たされると集中力が途切れてしまいますね。

フィードバックループの遅延 コードを書いて結果を確認するまでの時間が長いと、バグの原因特定や UI の微調整に時間がかかってしまいます。

開発意欲の低下 待ち時間が長いと、「後でまとめて確認しよう」という心理が働き、小さな変更を積み重ねてしまいがちです。 結果的に、問題の切り分けが難しくなり、開発効率がさらに低下するという悪循環に陥ってしまいます。

測定が必要な指標

最適化の効果を確認するため、以下の指標を計測しておくことが重要です。

#指標説明目標値
1初回起動時間yarn storybook から起動完了まで30 秒以内
2HMR 反映時間ファイル保存から反映まで2 秒以内
3ビルドサイズバンドルファイルのサイズ計測・監視
4メモリ使用量Node プロセスのメモリ計測・監視

これらの指標を最適化前後で比較することで、改善効果を定量的に評価できます。

解決策

HMR の遅延を解決するため、段階的に実装できる最適化手法をご紹介します。 すべてを一度に適用する必要はなく、プロジェクトの状況に応じて選択的に実装していくことをお勧めします。

最適化アプローチの全体像

最適化は以下の順序で進めると効果的です。

mermaidflowchart TD
  start["最適化開始"] --> measure["現状の計測"]
  measure --> s1["Story の<br/>遅延読み込み"]
  s1 --> s2["addon の<br/>最適化"]
  s2 --> s3["ビルド設定の<br/>チューニング"]
  s3 --> s4["TypeScript<br/>最適化"]
  s4 --> verify["効果の検証"]
  verify --> decision{"目標達成?"}
  decision -->|はい| done["最適化完了"]
  decision -->|いいえ| advanced["詳細調査<br/>プロファイリング"]
  advanced --> measure

図で理解できる要点:

  • 最適化は計測 → 実施 → 検証のサイクルで進める
  • 段階的に対策を積み重ねていく
  • 目標未達の場合は詳細調査に戻る

それでは、各最適化手法を具体的に見ていきましょう。

Story ファイルの遅延読み込み

最も効果的な最適化は、必要な Story だけを読み込む仕組みを導入することです。

従来の設定(全 Story を読み込み)

typescript// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  // すべての Story を一度に読み込む
  stories: [
    '../src/**/*.stories.@(js|jsx|ts|tsx)',
    '../src/**/*.story.@(js|jsx|ts|tsx)',
  ],
  // その他の設定...
};

この設定では、プロジェクト内のすべての Story ファイルが起動時に読み込まれ、メモリを大量に消費してしまいます。

最適化後の設定(パターンマッチング)

作業中のディレクトリだけを読み込むように環境変数で制御します。

typescript// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  stories: [
    // 環境変数で指定されたパスのみ読み込む
    process.env.STORYBOOK_STORIES_PATH ||
      '../src/**/*.stories.@(js|jsx|ts|tsx)',
  ],
  // その他の設定...
};

export default config;

package.json にスクリプトを追加

開発時に特定のディレクトリだけを対象にする npm スクリプトを用意します。

json{
  "scripts": {
    "storybook": "storybook dev -p 6006",
    "storybook:components": "STORYBOOK_STORIES_PATH='../src/components/**/*.stories.tsx' storybook dev -p 6006",
    "storybook:pages": "STORYBOOK_STORIES_PATH='../src/pages/**/*.stories.tsx' storybook dev -p 6006"
  }
}

これにより、作業中のコンポーネントに関連する Story だけを読み込めるようになり、起動時間と HMR 時間が大幅に短縮されます。

Addon の最適化

Storybook の addon は便利ですが、不要なものを読み込むと起動が遅くなります。

addon の見直し

現在使用している addon をリストアップし、本当に必要かを確認しましょう。

typescript// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';

const config: StorybookConfig = {
  addons: [
    // 必須の addon
    '@storybook/addon-essentials', // これは分割を検討

    // 開発時のみ必要な addon
    ...(process.env.NODE_ENV === 'development'
      ? ['@storybook/addon-a11y']
      : []),
  ],
};

export default config;

Essentials の分割読み込み

@storybook​/​addon-essentials は複数の addon をバンドルしています。 必要なものだけを個別に読み込むことで、初期化時間を削減できます。

typescript// .storybook/main.ts
const config: StorybookConfig = {
  addons: [
    // Essentials を分割して必要なものだけ読み込む
    '@storybook/addon-links',
    '@storybook/addon-controls',
    '@storybook/addon-actions',
    '@storybook/addon-viewport',
    // 以下は必要に応じてコメントアウト
    // '@storybook/addon-backgrounds',
    // '@storybook/addon-toolbars',
    // '@storybook/addon-measure',
    // '@storybook/addon-outline',
  ],
};

この変更により、不要な addon の初期化処理がスキップされ、起動時間が短縮されます。

Vite の最適化設定

Storybook 7 以降で推奨される Vite を使用している場合、ビルド設定を最適化できます。

基本的な Vite 最適化

開発環境では、ソースマップの生成を簡略化し、依存関係の事前バンドルを活用します。

typescript// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
import { mergeConfig } from 'vite';

const config: StorybookConfig = {
  framework: '@storybook/react-vite',

  async viteFinal(config) {
    return mergeConfig(config, {
      // ソースマップを軽量化
      build: {
        sourcemap: false,
      },

      // 開発サーバーの最適化
      server: {
        fs: {
          strict: false,
        },
      },
    });
  },
};

export default config;

依存関係の事前バンドル設定

頻繁に変更されない外部ライブラリを事前バンドルすることで、HMR の範囲を限定します。

typescript// .storybook/main.ts
async viteFinal(config) {
  return mergeConfig(config, {
    optimizeDeps: {
      include: [
        // よく使うライブラリを事前バンドル
        'react',
        'react-dom',
        'lodash',
        'date-fns',
      ],
      exclude: [
        // 開発中のパッケージは除外
        '@your-company/ui-components',
      ],
    },
  });
}

この設定により、外部ライブラリの再ビルドがスキップされ、自分のコードの変更だけが HMR で反映されるようになります。

Webpack を使用している場合の最適化

Webpack を使用している場合は、以下の設定が効果的です。

キャッシュの有効化

Webpack 5 のキャッシュ機能を活用して、ビルド時間を短縮します。

typescript// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-webpack5';

const config: StorybookConfig = {
  framework: '@storybook/react-webpack5',

  webpackFinal: async (config) => {
    // Webpack のキャッシュを有効化
    config.cache = {
      type: 'filesystem',
      cacheDirectory: '.webpack-cache',
    };

    return config;
  },
};

export default config;

SWC による高速トランスパイル

Babel の代わりに SWC を使用することで、トランスパイル速度が大幅に向上します。

bashyarn add -D @swc/core swc-loader
typescript// .storybook/main.ts
const config: StorybookConfig = {
  webpackFinal: async (config) => {
    // Babel を SWC に置き換え
    config.module.rules = config.module.rules.map(
      (rule) => {
        if (rule.test?.toString().includes('tsx')) {
          return {
            ...rule,
            use: [
              {
                loader: 'swc-loader',
                options: {
                  jsc: {
                    parser: {
                      syntax: 'typescript',
                      tsx: true,
                    },
                    transform: {
                      react: {
                        runtime: 'automatic',
                      },
                    },
                  },
                },
              },
            ],
          };
        }
        return rule;
      }
    );

    return config;
  },
};

SWC は Rust で書かれており、Babel よりも 10〜20 倍高速にトランスパイルできます。

TypeScript の型チェック最適化

TypeScript の型チェックは HMR のボトルネックになることがあります。

型チェックの分離

開発時は型チェックをスキップし、別プロセスで実行する方法が効果的です。

typescript// .storybook/main.ts
const config: StorybookConfig = {
  typescript: {
    // HMR 時の型チェックを無効化
    check: false,

    // ドキュメント生成のみ有効化
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      shouldExtractLiteralValuesFromEnum: true,
      propFilter: (prop) => {
        if (prop.parent) {
          return !prop.parent.fileName.includes(
            'node_modules'
          );
        }
        return true;
      },
    },
  },
};

export default config;

バックグラウンドでの型チェック

型チェックは別のターミナルで並行実行します。

json{
  "scripts": {
    "storybook": "storybook dev -p 6006",
    "storybook:typecheck": "tsc --noEmit --watch",
    "storybook:dev": "concurrently \"yarn storybook\" \"yarn storybook:typecheck\""
  }
}
bashyarn add -D concurrently

この設定により、HMR は高速に動作しながら、型エラーもバックグラウンドで検出できるようになります。

具体例

実際の大型プロジェクトで最適化を実施した際の、設定ファイルの完全な例をご紹介します。

プロジェクト概要

以下のような規模のプロジェクトを想定します。

#項目
1Story ファイル数約 400 ファイル
2コンポーネント数約 250 個
3最適化前の HMR 時間8〜12 秒
4最適化後の HMR 時間1〜2 秒

それでは、実際に使用した設定ファイルを見ていきましょう。

最適化された main.ts の完全な例

Vite を使用した Storybook 7 の設定例です。

typescript// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
import { mergeConfig } from 'vite';
import path from 'path';

/**
 * 環境変数から Story のパスを取得
 * 未指定の場合はすべての Story を読み込む
 */
const getStoriesPath = (): string[] => {
  const customPath = process.env.STORYBOOK_STORIES_PATH;

  if (customPath) {
    return [customPath];
  }

  // デフォルトはすべての Story を読み込む
  return ['../src/**/*.stories.@(js|jsx|ts|tsx)'];
};

const config: StorybookConfig = {
  // Story ファイルのパス
  stories: getStoriesPath(),

  // 必要最小限の addon のみ読み込む
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-controls',
    '@storybook/addon-actions',
    '@storybook/addon-viewport',
    // 開発時のみ a11y チェックを有効化
    ...(process.env.NODE_ENV === 'development'
      ? ['@storybook/addon-a11y']
      : []),
  ],

  // フレームワーク設定
  framework: {
    name: '@storybook/react-vite',
    options: {},
  },

  // TypeScript 設定
  typescript: {
    // HMR 中の型チェックを無効化(別プロセスで実行)
    check: false,
    reactDocgen: 'react-docgen-typescript',
    reactDocgenTypescriptOptions: {
      shouldExtractLiteralValuesFromEnum: true,
      propFilter: (prop) => {
        if (prop.parent) {
          return !prop.parent.fileName.includes(
            'node_modules'
          );
        }
        return true;
      },
    },
  },

  // Vite の詳細設定
  async viteFinal(config) {
    return mergeConfig(config, {
      // ビルド最適化
      build: {
        // 開発時はソースマップを無効化
        sourcemap: false,
        // チャンクサイズの警告を調整
        chunkSizeWarningLimit: 1000,
      },

      // 開発サーバー設定
      server: {
        fs: {
          strict: false,
        },
      },

      // 依存関係の最適化
      optimizeDeps: {
        include: [
          // 頻繁に使用する外部ライブラリを事前バンドル
          'react',
          'react-dom',
          'lodash',
          'date-fns',
          'classnames',
        ],
        exclude: [
          // 開発中の自社パッケージは HMR 対象にする
          '@your-company/ui-components',
        ],
      },

      // エイリアス設定
      resolve: {
        alias: {
          '@': path.resolve(__dirname, '../src'),
          '@components': path.resolve(
            __dirname,
            '../src/components'
          ),
          '@hooks': path.resolve(__dirname, '../src/hooks'),
        },
      },
    });
  },

  // 静的ファイルのディレクトリ
  staticDirs: ['../public'],

  // ドキュメント設定
  docs: {
    autodocs: 'tag',
  },
};

export default config;

この設定ファイルのポイントは以下の通りです。

環境変数による Story の絞り込み getStoriesPath 関数で、開発時に特定のディレクトリだけを対象にできます。

addon の最小化 本当に必要な addon だけを読み込み、a11y チェックなどは開発時のみ有効化しています。

TypeScript の型チェック分離 HMR のブロッキングを防ぐため、型チェックを無効化し、別プロセスで実行する前提です。

Vite の最適化 依存関係の事前バンドルとソースマップの無効化により、ビルド時間を短縮しています。

package.json のスクリプト例

開発ワークフローを効率化するための npm スクリプトです。

json{
  "scripts": {
    "storybook": "storybook dev -p 6006",
    "storybook:build": "storybook build",
    "storybook:components": "STORYBOOK_STORIES_PATH='../src/components/**/*.stories.tsx' storybook dev -p 6006",
    "storybook:pages": "STORYBOOK_STORIES_PATH='../src/pages/**/*.stories.tsx' storybook dev -p 6006",
    "storybook:typecheck": "tsc --noEmit --watch --project tsconfig.storybook.json",
    "storybook:dev": "concurrently \"yarn storybook:components\" \"yarn storybook:typecheck\"",
    "storybook:test": "test-storybook"
  },
  "devDependencies": {
    "@storybook/addon-a11y": "^7.6.0",
    "@storybook/addon-actions": "^7.6.0",
    "@storybook/addon-controls": "^7.6.0",
    "@storybook/addon-links": "^7.6.0",
    "@storybook/addon-viewport": "^7.6.0",
    "@storybook/react": "^7.6.0",
    "@storybook/react-vite": "^7.6.0",
    "concurrently": "^8.2.0",
    "storybook": "^7.6.0",
    "vite": "^5.0.0"
  }
}

各スクリプトの説明

  • storybook: 通常の Storybook 起動(全 Story 読み込み)
  • storybook:components: コンポーネントディレクトリのみ
  • storybook:pages: ページディレクトリのみ
  • storybook:typecheck: バックグラウンドでの型チェック
  • storybook:dev: Story の起動と型チェックを並行実行

tsconfig.storybook.json の設定

Storybook 専用の TypeScript 設定ファイルを用意します。

json{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "noEmit": true,
    "skipLibCheck": true,
    "resolveJsonModule": true
  },
  "include": [
    "src/**/*.stories.tsx",
    "src/**/*.stories.ts",
    ".storybook/**/*"
  ],
  "exclude": ["node_modules", "dist", "build"]
}

この設定により、型チェックの対象を Story ファイルに限定し、チェック時間を短縮できます。

最適化前後の比較

実際の測定結果を表にまとめました。

#指標最適化前最適化後改善率
1初回起動時間65 秒28 秒★★★★ 57% 削減
2HMR 反映時間8〜12 秒1〜2 秒★★★★★ 85% 削減
3メモリ使用量2.8 GB1.2 GB★★★★ 57% 削減
4ビルドサイズ45 MB38 MB★★ 16% 削減

特に HMR 反映時間の改善が顕著で、開発体験が劇的に向上しました。

実運用での注意点

最適化を実運用に適用する際の注意点をまとめます。

チーム全体への周知

新しいスクリプトの使い方をドキュメント化し、チームメンバーに共有しましょう。 特に、作業するディレクトリに応じてスクリプトを使い分ける習慣を定着させることが重要です。

CI/CD での Story 全体のテスト

開発時は Story を絞り込んでいるため、CI/CD では必ず全 Story をビルド・テストする設定を追加してください。

yaml# .github/workflows/storybook.yml
name: Storybook Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'

      # すべての Story をビルド
      - run: yarn install
      - run: yarn storybook:build

      # Story のテストを実行
      - run: yarn storybook:test

定期的なパフォーマンス計測

プロジェクトの成長に伴い、パフォーマンスが再び低下する可能性があります。 月に一度程度、HMR 時間などの指標を計測し、必要に応じて追加の最適化を検討しましょう。

まとめ

Storybook の HMR が遅い問題は、大型プロジェクトで頻繁に発生する課題ですが、適切な最適化により劇的に改善できます。

本記事で紹介した最適化手法のまとめ

Story ファイルの遅延読み込みにより、不要なファイルの読み込みを回避できます。 環境変数を活用して、作業中のディレクトリだけを対象にすることで、起動時間と HMR 時間を大幅に短縮しましょう。

Addon の見直しと最小化も効果的です。 特に @storybook​/​addon-essentials を分割し、本当に必要な addon だけを読み込むことで、初期化時間を削減できます。

Vite や Webpack のビルド設定を最適化することで、依存関係の処理を効率化できます。 開発時はソースマップを無効化し、外部ライブラリを事前バンドルすることがポイントです。

TypeScript の型チェックを HMR から分離し、バックグラウンドで実行することで、変更の反映速度を維持しつつ型安全性も確保できます。

最適化のステップ

まずは現状のパフォーマンスを計測し、ボトルネックを特定しましょう。 その後、Story の絞り込みから始めて、段階的に他の最適化を適用していくことをお勧めします。 すべての最適化を一度に実施する必要はなく、プロジェクトの状況に応じて選択的に実装していってください。

開発体験の向上

HMR が高速になることで、コードを書いて即座に結果を確認できるようになります。 この快適な開発体験は、チーム全体の生産性向上につながり、より質の高いコンポーネントを効率的に開発できるようになるでしょう。

ぜひ、本記事で紹介した手法を試して、快適な Storybook 開発環境を構築してください。

関連リンク