Viteで到達するマイクロフロントエンドの極限:Module Federationの呪縛を断ち切る「外部モジュール × 型定義同期」の建築思想
長年、エンタープライズ領域のフロントエンド開発において、Webpackの `Module Federation` はマイクロフロントエンド(MFE)の銀の弾丸として君臨してきた。ランタイムでのモジュール動的ロード、依存関係の共有、そして独立したデプロイライフサイクル。これらは巨大化するモノリスなSPAを解体するための一つの正解であった。
しかし、Viteがもたらした圧倒的な開発体験(DX)とビルド速度を知るアーキテクトにとって、Webpackの重厚長大な設定とHMRのレイラニー(遅延)へ回帰することは、もはや技術的退歩に他ならない。
Vite環境下でModule Federationを実現しようと、非公式のプラグイン(例: `@originjs/vite-plugin-federation` 等)を導入した現場の苦悩を私は数多く見てきた。RollupのエコシステムとViteのESM(EcmaScript Modules)ベースのバンドル戦略において、ランタイム共有のコンテナ管理はしばしばハイドレーションの不整合や、ビルドキャッシュの汚染、さらには型安全性の崩壊という致命的なトレードオフを生む。
本稿では、Viteのネイティブな挙動(Rollupの `external` とネイティブESM)を極限までハックし、プラグインという名のブラックボックスに頼らずにModule Federationと同等の疎結合性を担保する、極めて堅牢でモダンな代替アーキテクチャを提示する。
—
1. アーキテクチャの核心:なぜ「ランタイム統合」を捨て「ビルド時外部化」を選ぶのか
WebpackのModule Federationは、ランタイム(ブラウザ上)でリモート側の `remoteEntry.js` をフェッチし、依存関係をネゴシエーションする。これは強力だが、以下のインフラストラクチャ的・運用的な負債を伴う。
1. バージョンの暗黙的依存: ホストとリモートの間で共有ライブラリのバージョン競合が発生した際、ランタイム例外のデバッグが極めて困難になる。
2. CORSとCDNの厳格な同期: リモート側のデプロイメントURLが変更された場合、ホスト側への影響範囲制御(フォールバック等)のハンドリングが煩雑。
3. Viteの思想との衝突: Viteは「開発時はプレバンドル(esbuild)、本番時はRollupによる静的最適化」という割り切りによって高速性を担保している。ランタイムでの動的モジュール解決を無理に組み込むと、Viteの強みであるツリーシェイキングや静的解析の恩恵がスポイルされる。
代替案:Static External ESM + Federated Types
我々が目指すべきは、「ビルド時は疎結合、ランタイム時は静的解決(またはCDN経由のネイティブESM)」というアプローチである。
- ホスト(Host App): リモートモジュールを `external` 指定し、CDN上のURLへマッピングする。
- リモート(Remote App): 単一の独立したSPA/UIコンポーネントライブラリとしてビルドされ、型定義(`d.ts`)と共にCDNへパブリッシュされる。
- CI/CDパイプライン: リモートのデプロイメント時に、型定義を中央のレジストリ(またはS3/GitHub Packages)へ自動同期し、ホスト側のビルド時型チェックを担保する。
この構成により、ランタイムの複雑性を完全に排除しつつ、マイクロフロントエンドの本質である「チーム間のデプロイ独立性」と「型安全性の維持」を最高速度で両立できる。
—
2. 実装:Vite設定とRollup外部化の極意
まずは、リモート側コンポーネントをホスト側から安全に利用するためのVite設定を構築する。ここでは、リモート側が提供するコンポーネント群を `remote-ui-lib` という名前で外部化するケースを想定する。
リモート側(Remote App)の `vite.config.ts`
リモート側は、自身のUIコンポーネントをライブラリモードでビルドし、エントリポイントを明確に定義する。
import { defineConfig } from ‘vite’;
import react from ‘@vitejs/plugin-react’;
import dts from ‘vite-plugin-dts’;
import { resolve } from ‘path’;
export default defineConfig({
plugins: [
react(),
// 型定義(.d.ts)を自動生成し、配布物に含めるためのプラグイン
dts({
insertTypesEntry: true,
exclude: [‘src/App.tsx’, ‘src/main.tsx’, ‘src/vite-env.d.ts’],
}),
],
build: {
lib: {
// 外部公開するモジュールのエントリーポイント
entry: resolve(__dirname, ‘src/index.ts’),
name: ‘RemoteUI’,
// 出力ファイル名(ESM形式を強制)
fileName: ‘remote-ui’,
formats: [‘es’],
},
rollupOptions: {
// ホスト側と重複させない共通依存関係(React等)を外部化
// ホスト側が確実に提供していることが前提となる
external: [‘react’, ‘react-dom’],
output: {
globals: {
react: ‘React’,
‘react-dom’: ‘ReactDOM’,
},
},
},
},
});
ホスト側(Host App)の `vite.config.ts`
ホスト側では、Rollupの `external` オプションと、インポートマップ(またはViteのプラグインによるURL解決)を用いて、リモートモジュールをCDNのエンドポイントへ向ける。
import { defineConfig } from ‘vite’;
import react from ‘@vitejs/plugin-react’;
import { externalizeDepsPlugin } from ‘vite-plugin-externalize-deps’;
// 本番環境と開発環境でリモートの向き先を切り替えるCDN/オリジンURL
const REMOTE_BASE_URL = process.env.VITE_REMOTE_BASE_URL || ‘http://localhost:5001’;
export default defineConfig({
plugins: [
react(),
{
name: ‘vite-plugin-remote-resolver’,
// 開発・ビルド時にリモートモジュールのインポートをCDN URLに書き換えるカスタムプラグイン
resolveId(source) {
if (source === ‘remote-ui-lib’) {
return { id: `${REMOTE_BASE_URL}/remote-ui.js`, external: true };
}
},
},
],
build: {
rollupOptions: {
external: [
// ビルド時に解決せず、実行時/CDNからロードさせる外部モジュール
‘remote-ui-lib’,
],
},
},
});
—
3. 致命的な課題の克服:型定義(`d.ts`)の動的同期メカニズム
モジュールをCDN経由で外部化するアプローチの最大の懸念点は、「コンパイル時の型安全性が失われること」である。リモート側のインターフェースが変更された際、ホスト側のTypeScriptコンパイラがそれを検知できなければ、本番環境で予期せぬランタイムエラーを引き起こす。
これを解決するのが、CI/CDパイプラインにおける型定義の自動同期スクリプトである。
リモート側がビルドされるたびに、生成された `dist/index.d.ts` をプライベートNPMレジストリ、またはS3等のオブジェクトストレージへアップロードし、ホスト側のビルドプロセスで最新の型定義を自動取得する仕組みを構築する。
リモート側のCI/CDで実行する型定義パブリッションスクリプト (`scripts/publish-types.sh`)
!/usr/bin/env bash
set -euo pipefail
スクリプトの意図: リモート側で生成された型定義を、
ホスト側や型定義レジストリから参照可能なS3バケットへ自動アップロードする。
S3_BUCKET=”s3://my-company-mfe-types-registry”
VERSION=$(node -p “require(‘./package.json’).version”)
MODULE_NAME=”remote-ui-lib”
echo “===> Building types for ${MODULE_NAME} v${VERSION}…”
npm run build
echo “===> Uploading d.ts to registry…”
バージョン別と、常に最新を参照するlatestの2つのパスにアップロード
aws s3 cp dist/index.d.ts “${S3_BUCKET}/${MODULE_NAME}/${VERSION}/index.d.ts”
aws s3 cp dist/index.d.ts “${S3_BUCKET}/${MODULE_NAME}/latest/index.d.ts”
echo “===> Type publication completed successfully.”
ホスト側で型定義を自動同期するCLIツール (`scripts/sync-types.mjs`)
ホスト側の `package.json` の `prepare` スクリプト等から実行し、ビルド前に最新の型定義をローカルの `@types/remote-ui-lib` として配置する。
import { mkdirSync, writeFileSync } from ‘fs’;
import { resolve } from ‘path’;
import https from ‘https’;
// スクリプトの意図: リモート側の最新型定義をCDN/S3からフェッチし、
// ホスト側のTypeScriptコンパイラが認識できる型定義ディレクトリに自動配置する。
const TYPES_URL = process.env.REMOTE_TYPES_URL || ‘https://cdn.example.com/remote-ui-lib/latest/index.d.ts’;
const OUTPUT_DIR = resolve(process.cwd(), ‘src/@types/remote-ui-lib’);
async function syncTypes() {
console.log(`===> Fetching remote types from: ${TYPES_URL}`);
https.get(TYPES_URL, (res) => {
if (res.statusCode !== 200) {
console.error(`Failed to fetch types. Status Code: ${res.statusCode}`);
process.exit(1);
}
let data = ”;
res.on(‘data’, (chunk) => { data += chunk; });
res.on(‘end’, () => {
mkdirSync(OUTPUT_DIR, { recursive: true });
// TypeScriptがモジュールとして認識するためのambient module宣言を付与して保存
const wrappedContent = `
declare module ‘remote-ui-lib’ {
${data}
}
`;
writeFileSync(resolve(OUTPUT_DIR, ‘index.d.ts’), wrappedContent);
console.log(‘===> Remote types synchronized successfully.’);
});
}).on(‘error’, (err) => {
console.error(`Error downloading types: ${err.message}`);
process.exit(1);
});
}
syncTypes();
ホスト側の `tsconfig.json` にて、このカスタム型定義ディレクトリをパスとして解決させる。
{
“compilerOptions”: {
“paths”: {
“remote-ui-lib”: [“./src/@types/remote-ui-lib”]
}
}
}
この仕組みにより、リモート側の破壊的変更(Propsの変更や削除)は、ホスト側のビルドフェーズ(CI/CDのTypeScript型チェック)で100%検知され、ビルドが失敗するようになる。
—
4. Dockerコンテナ環境における完全自動構成とCI/CDパイプライン
このアーキテクチャを本番運用する際、ローカル開発環境(Docker Compose)および本番CI/CD(GitHub Actions等)でのネットワーク解決が鍵となる。
特にDocker環境下では、ホストとリモートが異なるコンテナとして起動する場合、Viteの開発サーバー間でのCORS設定とポートフォワーディングの厳密な制御が必要不可欠である。
Docker Composeによるローカル統合開発環境 (`docker-compose.yml`)
version: ‘3.8’
services:
# リモートUIコンポーネントを提供する独立したコンテナ
remote-app:
build:
context: ./apps/remote-app
dockerfile: Dockerfile.dev
ports:
- “5001:5001”
environment:
- PORT=5001
volumes:
- ./apps/remote-app/src:/app/src
# リモートアプリを消費するホストアプリケーション
host-app:
build:
context: ./apps/host-app
dockerfile: Dockerfile.dev
ports:
- “3000:3000”
environment:
- VITE_REMOTE_BASE_URL=http://localhost:5001
depends_on:
- remote-app
volumes:
- ./apps/host-app/src:/app/src
パフォーマンスとメモリ最適化のハック
Vite + 外部モジュール構成における最大のパフォーマンスハックは、「Viteの依存関係事前バンドル(`optimizeDeps`)からの除外」である。
もしホスト側がリモートモジュールを通常のローカルパッケージとして読み込もうとすると、Viteはそれをスキャンして `node_modules/.vite` 内にキャッシュしようとし、循環参照やビルドの肥大化を招く。
ホスト側の `vite.config.ts` において、以下のように最適化設定を施すこと。
export default defineConfig({
optimizeDeps: {
// 外部化したリモートモジュールをプレバンドルの対象外に強制指定
exclude: [‘remote-ui-lib’],
},
});
これにより、Viteの起動時間およびHMRの反応速度は、コードベースの規模がどれほど巨大になっても数ミリ秒単位の俊敏性を維持し続ける。
—
エキスパートからの総括
Webpackの Module Federation は強力だが、それは「Webpackのアーキテクチャ制約の中で複雑な問題を解決するためのもの」に過ぎない。
Viteが切り拓いたネイティブESMとRollupの堅牢なエコシステムを背景に持つ我々は、プラグインの魔法に頼る必要はない。「明確な境界線(External)」「自動化された型定義の同期(Federated Types)」「静的なCDN配信」。この3つを組み合わせることで、Module Federationの弱点である複雑性を完全に排除しつつ、マイクロフロントエンドの理想郷である「疎結合と型安全性の高次元での両立」を達成できる。
複雑怪奇なランタイム設定を捨て、ピュアで高速なビルドパイプラインへ回帰せよ。それこそが、現代のトップティアDevOpsエンジニアが選ぶべき最適解である。