こんにちは!開発現場で日々コードと向き合っていると、「あれ、なんか最近ビルド遅くない?」「なんかバンドルサイズが肥大化してない?」と気づく瞬間ってありませんか?
大規模な開発になると、複数のアプリや共通コンポーネントを1つのリポジトリで管理する「Monorepo(モノレポ)」という手法が主流になります。TurborepoやNxといった強力なオーケストレータツールのおかげで、環境構築やタスク実行は劇的に快適になりました。
しかし、ここで多くの開発者が「依存関係の重複(ホイスティングの罠)」という見えない壁にぶつかります。これを放置すると、ビルドが重くなるだけでなく、本番環境で予期せぬバグを踏む原因にもなります。
今回は、WebpackやViteを使ったMonorepo環境において、この依存関係の重複問題を美しく解決するワークスペース戦略を、僕と一緒に紐解いていきましょう。これをマスターすれば、あなたのプロジェクトのビルドパフォーマンスは見違えるほど軽快になりますよ!
—
1. なぜMonorepoで「依存関係の重複」が起きるのか?
まずは敵を知ることから始めましょう。Monorepo(ここでは pnpm や Yarn, npm のワークスペース機能を利用した環境を想定)では、次のようなディレクトリ構成をとることが多いです。
my-monorepo/
┣ apps/
┃ ┣ web-app/ # メインのWebアプリ (React/Vue等)
┃ ┗ admin-app/ # 管理画面アプリ
┗ packages/
┗ ui-components/ # 共通のUIコンポーネント
ここで問題になるのが、「依存ライブラリのバージョン不一致」や「パッケージマネージャのホイスティング(Hoisting)の挙動」です。
例えば、`apps/web-app` も `packages/ui-components` も、内部で `react` というライブラリを使うとします。
もし、それぞれの `package.json` で微妙に依存するバージョン範囲が異なったり、パッケージマネージャの解決アルゴリズムの都合でルート直下(`node_modules`)ではなく各パッケージ配下の `node_modules` にライブラリがクローン・配置されてしまうとどうなるでしょう?
WebpackやViteがバンドル(ビルド)を行う際、これらを「別々の実体」として誤認し、同じReactライブラリが何重にも最終的なバンドルファイルに含まれてしまうという悲劇が起きます。これが、バンドルサイズを爆発させ、メモリ消費量を跳ね上げる元凶です。
—
2. 解決のアプローチ:ワークスペース戦略の全体像
この問題を根本からねじ伏せるためには、次の2つのアプローチを組み合わせたアーキテクチャ設計が必要です。
1. パッケージマネージャレベルでの強制ホイスティング(pnpmなら `pnpm-workspace.yaml` と `.npmrc` の調整)
2. バンドラレベルでのパス解決の単一化(Webpackの `resolve.alias` や Viteの `resolve.dedupe` の活用)
今回は、特に現場でトラブルになりやすい 「Webpack」 と、モダンな爆速ビルドツール 「Vite」 の双方で、この重複をねじ伏せる具体的な設定を見ていきましょう。
—
3. 実践!依存関係を美しくコントロールする設定術
ここでは、共通UIパッケージ(`@repo/ui`)と、それを読み込むWebアプリ(`@repo/web`)が存在する世界線を想定します。
ステップ1:パッケージマネージャ(pnpmを例に)の厳格な制御
まず、大元のパッケージマネージャがバラバラに依存関係を巻き取らないよう、ルートディレクトリの `.npmrc` で依存関係の孤立を防ぎます。
.npmrc
シンボリックリンクの張り方を調整し、幽霊依存(Phantom dependencies)を防ぎつつホイスティングを最適化する
shamefully-hoist=false
public-hoist-pattern[]=react
public-hoist-pattern[]=react-dom
- 解説: Reactのようなコアライブラリは、複数バージョンが混ざるとHooksの挙動がおかしくなる(「Invalid hook call」エラー)原因になります。そのため、明示的にホイスティングパターンを指定して、常に単一のバージョンが参照される土台を作ります。
—
ステップ2:Webpack環境での `resolve.alias` による単一化
Webpackを使っているレガシー、あるいは堅牢なプロジェクトでは、ビルド時に「どのパスのReactを読みに行くか」を強制的に一意に固定します。
以下は、`apps/web-app/webpack.config.js` の設定例です。
const path = require(‘path’);
module.exports = {
// 中略(entryやoutputの設定)
resolve: {
alias: {
// モノレポ内の複数箇所からreactが読み込まれるのを防ぐため、ルートのreactにパスを強制固定する
react: path.resolve(__dirname, ‘../../node_modules/react’),
‘react-dom’: path.resolve(__dirname, ‘../../node_modules/react-dom’),
},
extensions: [‘.tsx’, ‘.ts’, ‘.js’, ‘.jsx’],
},
};
- 解説: `path.resolve` を使って、アプリケーション独自の `node_modules` ではなく、モノレポのルート(大元)に存在する `node_modules/react` を指し示すようにエイリアス(別名)を強制しています。これにより、Webpackは迷うことなく同一のインスタンスをバンドルに組み込みます。
—
ステップ3:Vite環境での `resolve.dedupe` による重複排除
では、次世代の高速ビルドツールである Vite ではどうでしょうか?
Viteは標準で非常に賢いですが、モノレポ環境でシンボリックリンク経由でローカルパッケージ(`@repo/ui`など)を読み込む際、依存関係が重複することがあります。
Viteには、まさにこの問題を解決するための神機能 `resolve.dedupe` が備わっています。
以下は、`apps/web-app/vite.config.ts` の設定例です。
import { defineConfig } from ‘vite’;
import react from ‘@vitejs/plugin-react’;
import path from ‘path’;
export default defineConfig({
plugins: [react()],
resolve: {
// Viteに「これらのパッケージは、依存先で重複して見つかっても、必ずトップレベルのものを強制的に使え」と指示する
dedupe: [‘react’, ‘react-dom’],
alias: {
// 必要に応じてローカルパッケージのパスを明示
‘@repo/ui’: path.resolve(__dirname, ‘../../packages/ui-components/src’),
},
},
});
- 解説: `dedupe` プロパティに配列形式で指定されたパッケージは、Viteの依存関係事前バンドル(Dependency Optimization)の段階で重複が検出された場合、単一のバージョンに強制的にまとめられます。これによって、開発サーバーの起動速度低下や、プロダクションビルド時のバンドル肥大化を一発で防ぐことができます。
—
4. 動作確認:バンドルサイズを自分の目で暴く
設定が終わったら、本当に重複が解消されたかを確認しましょう。これを怠るエンジニアは三流です。一流のエンジニアは必ず「数字」で成果を証明します。
Webpackなら `webpack-bundle-analyzer`、Viteなら `rollup-plugin-visualizer` を導入して、ビルド結果をビジュアル化します。
Viteの場合の導入例:
可視化プラグインをインストール
npm install rollup-plugin-visualizer –save-dev
`vite.config.ts` に追加:
import { visualizer } from ‘rollup-plugin-visualizer’;
export default defineConfig({
plugins: [
react(),
// ビルド時にHTML形式のレポートを出力する
visualizer({
open: true, // ビルド完了時に自動でブラウザでレポートを開く
filename: ‘stats.html’,
gzipSize: true,
brotliSize: true,
}),
],
});
この状態でビルドコマンド(例: `pnpm build`)を実行してみてください。
ブラウザ上にカラフルなモザイク状の画面(ビルド解析レポート)が立ち上がります。そこに `react` や大容量の外部ライブラリが「別々の名前で複数個」ポツポツと存在していなければ、あなたの勝利です!綺麗に1つに統合されているはずです。
—
まとめ:日々の開発が劇的に快適に
今回は、Monorepo環境における最大の罠である「依存関係の重複(Hoisting)」と、それをWebpack・Viteそれぞれの作法でねじ伏せるワークスペース戦略について解説しました。
- Monorepoではパッケージの重複がバンドルサイズ肥大化の最大の原因になる
- パッケージマネージャ側でホイスティングのルールを整える
- Webpackなら `resolve.alias`、Viteなら `resolve.dedupe` で強烈にパスを一本化する
- 必ずビジュアライザーツールで結果を視覚的に検証する
このアーキテクチャの引き出しを持っていれば、どれだけ複雑なモノレポ規模のアプリケーションを任されても、ビルドパフォーマンスで頭を悩ませることはもうありません。
これをマスターすれば、あなたの毎日のコーディング、そしてデプロイ待ちの時間が劇的に快適になりますよ。ぜひ、今日のプロジェクトから試してみてくださいね!