マイクロフロントエンド(Micro Frontends)は、1つのWebアプリの画面を業務の区切りごとに別チームが開発し、別々にデプロイしたうえで、1つの製品として組み立てる設計手法です。バックエンドのマイクロサービスと同じ発想をフロントエンドに当てはめたもので、狙いはチームごとの独立したリリースにあります。この記事では、定義、垂直分割と水平分割の選び方、5つの統合方式、アプリ間の通信、Module Federationの実装、ツールの選び方、導入を見送るべき条件までを順に説明します。コードは2026年11月時点のRspack 2.3.2と @module-federation/enhanced 2.9.3でビルドし、ヘッドレスのChrome 158で動かした結果に基づきます。
まとめ:マイクロフロントエンドの要点
- 定義は「独立して届けられるフロントエンドアプリを組み合わせて1つの製品にする設計」。ThoughtWorksが2016年11月に技術レーダーへ載せた用語です。
- 分け方はURLごとに1アプリを置く垂直分割から始めます。1画面に複数チームの部品を並べる水平分割では、共有する依存の版やCSSの境界を調整する負担が増えるため、必要な画面に限ります。
- 統合方式は、サーバー側合成・ビルド時統合・iframe・JavaScript(Module Federationなど)・Web Componentsの5つ。ビルド時統合は一斉リリースが必要になり、独立デプロイの利点が消えるので選びません。
- アプリ間の通信は、URL・カスタムイベント・Cookieで足りるように設計し、状態の直接共有は避けます。Module Federationで状態を持つライブラリを共有するなら
singletonとstrictVersionを付けます。付けないと版が合わなくても警告だけで動き続けます。 - Next.js向けの
@module-federation/nextjs-mfは廃止予定でApp Router非対応です。Next.jsで分けるならMulti-Zones(Vercel上ならVercel Microfrontends)を検討します。 - 独立してリリースしたいチームが複数いなければ、モジュラモノリスやモノレポで足ります。
マイクロフロントエンドの定義とモノリス構成との違い
用語の出どころ:2016年の技術レーダーと2019年のFowler記事の定義
この用語は、ThoughtWorksの技術レーダー2016年11月号に「Assess(評価対象)」として掲載されたのが始まりです。レーダーのページでは、その後2017年11月号でTrial、2019年4月号でAdoptへ格上げされています。2019年6月19日にmartinfowler.comで公開されたCam Jacksonの記事「Micro Frontends」は、次のように定義しました。
An architectural style where independently deliverable frontend applications are composed into a greater whole
定義の核は「independently deliverable(独立して届けられる)」の部分です。画面を部品に分けただけで、リリースのたびに全チームがそろう必要があるなら、それはマイクロフロントエンドではありません。
モノリスSPAとの構造の違い
モノリスSPAは、画面全体を1つのアプリとしてビルド・デプロイします。リポジトリの数は、このデプロイ単位とは別に決められます。マイクロフロントエンドでは、検索・商品詳細・カートのように業務の機能ごとに担当チームが縦に切られ、各チームが画面からAPI呼び出しまでを持ちます。
| 観点 | モノリスSPA | マイクロフロントエンド |
|---|---|---|
| デプロイ単位 | アプリ全体 | 機能ごとのアプリ |
| リポジトリ | 単一・複数とも可 | 単一・複数とも可 |
| 依存ライブラリの版 | 全体で1つ | アプリごとに持てる |
| 障害の影響範囲 | 全体に及びやすい | 担当アプリに閉じやすい |
| 運用の手間 | 少ない | パイプライン・配信先が増える |
モノリスと比べたメリット:独立デプロイと段階的な更新
Cam Jacksonの記事は、メリットとして次の4つを挙げています。
- 段階的な更新:古いフレームワークで書かれた画面を、全体を止めずに1機能ずつ新しい技術へ置き換えられる。
- 小さく分離されたコードベース:1チームが読むコードが自分の機能の範囲に収まり、意図しない結合が生まれにくい。
- 独立したデプロイ:各アプリが専用のパイプラインで本番に出るため、他チームのリリース日を待たない。
- 自律したチーム:画面から裏側までを1チームが持ち、技術の判断を担当範囲の中で完結できる。
段階的な更新とコードベースの分離は1チームでも効きますが、独立したデプロイと自律したチームは、チームが複数あって初めて効くメリットです。記事は裏側についても、マイクロフロントエンドごとに専用のバックエンドを置くBFFパターンを勧めています(BFFの設計はBFFとは?API Gatewayとの違い・採用判断とアンチパターンを実装視点で解説で扱っています)。
分割の単位:垂直分割と水平分割の選び方
どこで画面を切るかは、統合方式より先に決めます。切り方は大きく2つあります。
| 観点 | 垂直分割 | 水平分割 |
|---|---|---|
| 単位 | URL(ページ)ごとに1アプリ | 1画面に複数アプリの部品 |
| 主な統合手段 | ルーティング・サーバー側合成 | Module Federation・Web Components |
| 依存の共有 | 必須ではない | 任意(共有する場合は版を調整) |
| アプリ間の遷移 | ページの読み直しになりやすい | 同じページ内で完結 |
| チーム間の調整 | 少ない | レイアウト・読み込み順で多い |
垂直分割:URL単位の画面所有とチーム境界
Cam Jacksonの記事は、チームを技術の層ではなく業務の縦の切れ目で組み、利用者に見える単位で製品を切る、つまり「1つのマイクロフロントエンドがアプリの1ページを持ち、1チームが端から端まで所有する」のが分かりやすいやり方だとしています。/search は検索チーム、/checkout は決済チームという形なら、各アプリは自分のReactの版を持ってよく、共有の交渉も起きません。後述のNext.jsのMulti-Zones、Vercel Microfrontends、single-spaのルート単位の登録は、どれもこの垂直分割の道具です。
水平分割:同一画面への部品配置と調整負担
商品詳細ページの中に、レコメンド担当チームの枠とカート担当チームのバッジを並べるような切り方です。画面の中で複数チームの部品が同時に動くので、CSSの衝突、読み込みの遅れ、共有する依存の版の調整が問題になります。Reactは1ページに独立したコピーが複数あっても動きますが、React公式によると、コンポーネントが参照する react と、それを描画する react-dom が参照する react が別物だとHooksが壊れます。remoteのコンポーネントをhostのReactで描画するなら、Reactの共有が必要です。水平分割は、同じ画面に別チームの機能が常に並ぶ場合(ECのヘッダーのカート表示など)に限ります。迷ったら垂直分割で始め、水平に切る部品は1〜2個から試します。
統合方式5つの比較:タイミングと分離の強さ
Cam Jacksonの記事は、統合のやり方を次の5つに分けています(表の評価と方式の具体例は、この記事での整理です)。違いは「いつ組み立てるか」と「アプリ同士をどこまで隔離するか」です。
| 方式 | 組み立てる時点 | 隔離の強さ | 依存の共有 |
|---|---|---|---|
| サーバー側テンプレート合成 | リクエスト時(サーバー) | 中 | しにくい |
| ビルド時統合(npmパッケージ) | ビルド時 | 弱い | しやすい |
| iframe | 実行時(ブラウザ) | 強い | できない |
| JavaScript(Module Federation・single-spa) | 実行時(ブラウザ) | 弱い | しやすい |
| Web Components | 実行時(ブラウザ) | 中(Shadow DOM) | 方式しだい |
サーバー側・エッジでの合成:SSIとパス単位のルーティング
最も古い方式は、サーバーが複数のHTML断片を1枚のページにまとめて返すやり方です。Cam Jacksonの記事の例では、共通部分だけを持つ index.html にNginxのSSI(<!--# include file="$PAGE.html" -->)でページごとの断片を差し込みます。本文を含むHTMLを返せるため、初期表示や検索エンジンによる内容取得に利点がありますが、表示速度はサーバー処理やキャッシュにも左右され、断片を出すチームのサーバーが遅いとページ全体が待たされます。これとは別に、CDNやエッジがURLのパスを見て別々のアプリへ振り分ける方式もあります。後述のVercel Microfrontendsはこのルーティングを提供し、HTML断片の合成とは区別されます。
ビルド時統合を選ばない理由
各アプリをnpmパッケージとして公開し、コンテナのpackage.jsonに並べる方式は、共通依存の重複が消えて扱いやすく見えます。しかし記事は、どれか1つの機能を出すたびに全アプリを再コンパイルして一斉にリリースする必要が生じるとして、この方式を強く勧めないと書いています。独立デプロイがマイクロフロントエンドの目的である以上、ビルド時統合はモノレポで1つのアプリを分割管理するのと変わりません。統合は実行時に行う、が出発点です。
iframeとWeb Componentsの隔離の違い
iframeは別文書として読み込むため、CSSもグローバル変数も混ざりません。そのかわり、別オリジンで配信する場合、親子間のやり取りは postMessage に限られ、高さの自動調整やディープリンクに手間がかかります。管理画面に外部の帳票ツールを埋め込むような、境界がはっきりした場面に向きます(sandbox属性の設計はiframeとは?sandbox属性14種とCookie分割の実装判断を解説で扱っています)。
Web Componentsは同じ文書の中でカスタム要素として動き、Shadow DOMでスタイルを閉じ込められます。次の例は、カート担当チームが配布するバッジを、商品担当チームが CustomEvent だけで更新する構成です。
<cart-badge count="0"></cart-badge>
<script type="module">
// カート担当チームが配布するカスタム要素
class CartBadge extends HTMLElement {
static observedAttributes = ['count'];
connectedCallback() {
if (!this.shadowRoot) this.attachShadow({ mode: 'open' });
this.render();
}
attributeChangedCallback() {
if (this.shadowRoot) this.render();
}
render() {
this.shadowRoot.innerHTML =
`<style>span{color:red}</style><span>カート ${this.getAttribute('count')}件</span>`;
}
}
customElements.define('cart-badge', CartBadge);
// 商品担当チームとは CustomEvent だけでつなぐ
window.addEventListener('cart:added', (e) => {
document.querySelector('cart-badge').setAttribute('count', String(e.detail.total));
});
window.dispatchEvent(new CustomEvent('cart:added', { detail: { total: 3 } }));
</script>
Chrome 158(ヘッドレス)で開くと、バッジは「カート 3件」に変わりました。ページ側に別の span を置いて確かめると、Shadow DOM内の span{color:red} は外の span に及ばず黒(rgb(0, 0, 0))のままでした。Shadow DOMを使わずに同じスタイルを書くと、外の span も赤(rgb(255, 0, 0))に変わります。attachShadow() の前に shadowRoot の有無を見ているのは、コンテナがルーティングで部品を別の場所へ付け直すと connectedCallback がもう一度呼ばれ、2回目の attachShadow() が NotSupportedError になるためです。
アプリ間の通信と状態の置き場所
Cam Jacksonの記事は、マイクロフロントエンド同士の通信は「できるだけ少なく」するよう勧めています。通信が増えるほど、分けたはずのアプリが再び結合するからです。そのうえで手段として、カスタムイベント、コンテナから子へのデータとコールバックの受け渡し、アドレスバー(URL)の3つを挙げ、共有の状態は持たずにメッセージやイベントで伝えるよう書いています。状態の種類ごとに置き場所を決めておくと、この方針を守りやすくなります。
| 状態の種類 | 置き場所 | 読み書きの範囲 |
|---|---|---|
| ログイン状態・認証 | HttpOnly Cookie+BFF | サーバーが検証・各アプリは結果を読む |
| 表示中の対象(商品ID・検索条件) | URLのパス・クエリ | 全アプリが読む |
| 一時的な通知(カート追加) | CustomEvent | 送る側と受ける側だけ |
| コンテナが渡す設定(言語・テーマ) | 属性・props | コンテナが書き、子は読む |
| アプリ内の画面状態 | 各アプリのストア | そのアプリだけ |
Reduxを使う場合も、記事は各マイクロフロントエンドが自分のストアを持つ構成を妥当としています。全アプリで1つのストアを共有すると、ストアの形を変えるたびに全チームのリリースがそろう必要が出て、ビルド時統合と同じ問題に戻ります。どうしても同じインスタンスを共有するなら、次章のModule Federationの singleton を使い、版の食い違いを strictVersion で止めます。カスタムイベントは結合が弱い反面、イベント名と detail の形が暗黙の契約になるので、型定義か文書にして固定します。
Module Federationの実装:Rspackでhostとremoteを動かす手順
Module Federationは、別々にビルドしたアプリ同士が実行時にモジュールを公開・読み込みし、共通の依存を1つにまとめる仕組みです。2020年10月リリースのwebpack 5に組み込まれ、現在はwebpack・Rspack・Vite向けのプラグインとランタイムを @module-federation 配下で配布しています。公式ドキュメントは、このプロジェクトが提供する機能群を「Module Federation 2.0」と呼び(パッケージの版番号とは別の呼称です)、webpack 5内蔵版に動的な型ヒント、マニフェスト、Federation Runtime、ランタイムプラグインを加えたものと説明しています。ここではremote(部品を出す側)とhost(読み込む側)を、状態を持つ共有パッケージ @demo/store 1.2.0(読み込み時にIDを振るオブジェクト)でつなぎます。
const { ModuleFederationPlugin } = require('@module-federation/enhanced/rspack');
const rspack = require('@rspack/core');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: { publicPath: 'http://localhost:3001/', uniqueName: 'remote' },
plugins: [
new ModuleFederationPlugin({
name: 'remote',
filename: 'remoteEntry.js',
exposes: { './Widget': './src/Widget.js' },
shared: { '@demo/store': { singleton: true, requiredVersion: '^1.0.0' } },
dts: false,
}),
new rspack.HtmlRspackPlugin(),
],
};
exposes に書いたパスが、hostから remote/Widget として見えます。dts: false は型ファイルの生成と取得を止める指定で、TypeScriptで書くなら外してremoteの型をhostへ自動で配らせます。公開するモジュールは次のとおりです。
// remote/src/Widget.js
import { store } from '@demo/store';
export function mount(el) {
store.count += 1;
el.textContent = `remote widget: store=${store.id} count=${store.count}`;
return store;
}
host側の設定とbootstrapの分離
const { ModuleFederationPlugin } = require('@module-federation/enhanced/rspack');
const rspack = require('@rspack/core');
module.exports = {
mode: 'production',
entry: './src/index.js',
output: { publicPath: 'http://localhost:3000/', uniqueName: 'host' },
plugins: [
new ModuleFederationPlugin({
name: 'host',
remotes: { remote: 'remote@http://localhost:3001/remoteEntry.js' },
shared: { '@demo/store': { singleton: true, requiredVersion: '^1.0.0' } },
dts: false,
}),
new rspack.HtmlRspackPlugin(),
],
};
入口のファイルは動的 import() だけにして、実際の処理を bootstrap.js に移します。
// host/src/index.js(入口は動的importだけにする)
import('./bootstrap.js');
// host/src/bootstrap.js
import { store } from '@demo/store';
import { mount } from 'remote/Widget';
store.count += 1;
const el = document.createElement('div');
document.body.appendChild(el);
mount(el);
入口で bootstrap.js を静的にimportすると、共有モジュールの交渉が終わる前に読み込もうとして、ブラウザで次のエラーが出て止まりました。
[ Federation Runtime ]: Invalid loadShareSync function call from runtime #RUNTIME-006
args: {"hostName":"host","sharedPkgName":"@demo/store"}
ビルドと配信:共有パッケージの用意とtype指定
remoteの入口もhostと同じく動的importだけにします。remote側の入口、共有パッケージ、依存の導入、ビルド、配信は次のとおりです。
// remote/src/index.js(remote単体で開いたときの入口)
import('./bootstrap.js');
// remote/src/bootstrap.js
document.body.textContent = 'remote standalone';
// store/package.json(共有パッケージ。store・remote・host を同じ階層に並べる)
{"name":"@demo/store","version":"1.2.0","main":"index.js","type":"module"}
// store/index.js
export const store = { id: Math.random().toString(36).slice(2, 8), count: 0 };
# remote/ と host/ でそれぞれ実行(設定ファイルは rspack.config.cjs)
npm init -y
npm pkg set type=module
npm i @rspack/[email protected] @rspack/[email protected] @module-federation/[email protected] ../store
npx rspack build
# 配信(store・remote・host の親ディレクトリで、ターミナルを2つ開いて1行ずつ実行。remote 側はCORSを許可する)
npx http-server remote/dist -p 3001 --cors
npx http-server host/dist -p 3000
npm pkg set type=module を飛ばすと、npm 11.17.0の npm init -y が書き込む "type": "commonjs" のせいで、src の import 文が「Cannot use import statement outside a module」となりビルドが失敗しました。この手順で http://localhost:3000/ を開くと、画面に remote widget: store=… count=2 と出ます。
同じhostとremoteを設定だけ変えてビルドし、ヘッドレスのChrome 158で開いて、hostとremoteが同じストアを見ているかを調べました(2026年10月のRspack 2.2.8・enhanced 2.9.2でも同じ結果でした)。
| 構成 | 結果 |
|---|---|
| 両方で singleton・^1.0.0 | 同じインスタンス(count=2) |
| shared 指定なし | 別インスタンス(各count=1) |
| remoteだけ ^2.0.0 を要求 | 警告のみで1.2.0を共有して動作 |
| 上記+strictVersion: true | 例外で描画されない |
| 入口でbootstrapを静的import | RUNTIME-006で停止 |
注意したいのは3行目です。remoteが2系を前提に書かれていても、コンソールに Version 1.2.0 from host of shared singleton module @demo/store does not satisfy the requirement of remote which needs ^2.0.0 と出るだけで、画面は1系で動きます。コンソールの警告を見落とすと、APIが変わる大きな版の違いに本番で初めて気づくことになります。Reactコンポーネントをアプリ間で受け渡す場合や、同じ状態オブジェクトの共有が必要な場合は、singletonに加えて次のように(remote側の例)strictVersion を付け、版の食い違いをその場で止めるほうが安全です。
// remote側:2系を前提に書いたコードの設定
shared: {
'@demo/store': { singleton: true, requiredVersion: '^2.0.0', strictVersion: true },
},
2行目は、同じストアを共有する設計なのにsharedを指定しなかった場合の結果です。この例ではhostとremoteが別々のストアを持ちますが、アプリごとにストアを分ける設計自体は誤りではありません。
single-spa・Native Federation・Next.jsの選び方
| パッケージ | npm現行版 | 公開日 |
|---|---|---|
| @module-federation/enhanced | 2.9.3 | 2026-11-03 |
| @module-federation/vite | 1.23.11 | 2026-11-06 |
| @module-federation/nextjs-mf | 8.9.0 | 2026-11-03 |
| single-spa | 6.0.3 | 2024-09-29 |
| @angular-architects/native-federation | 22.2.5 | 2026-11-11 |
| @vercel/microfrontends | 2.4.2 | 2026-10-20 |
2026年11月13日にnpmレジストリで確認した値です(公開日はUTC)。最新は各パッケージのページで確認してください。
single-spa:URL単位で複数フレームワークを切り替えるルーター
single-spaは、URLに応じてどのアプリを起動・破棄するかを管理するルーターです。root configと呼ぶ共通のHTMLと、registerApplication() を呼ぶJavaScriptだけを持ち、公式ドキュメントはその役割をアプリの起動に限るよう求めています。推奨構成はブラウザのES Modulesとimport mapsで、古いブラウザ向けにはSystemJSで補います。画面の一部だけを複数フレームワークで共有する部品はparcelとして作れますが、公式は主な単位をapplicationにするよう勧めています。React・Vue・Angularが混在する既存資産を、ルート単位で寄せ集めたいときに向きます。なお安定版は2024年9月の6.0.3が最新で、7系は2025年9月の7.0.0-beta.13を最後にbeta版のままです。
Native Federation:import mapsによるブラウザ標準での実装
@angular-architects/native-federation は、Module Federationの考え方をES Modulesとimport mapsというブラウザ標準で実装したものです。ビルドで remoteEntry.json と importmap.json を出力し、起動時にhostとremoteのimport mapを統合してページに差し込みます。このパッケージはAngular向けのアダプターで、本体の @softarc/native-federation(4.10.0)はビルドツールにもフレームワークにも依存しません。土台のimport mapsは、Baselineで2023年3月に主要ブラウザがそろい、2025年9月に「広く利用可能」になりました。仕組みは次の数行で確かめられます。
// /cart/1.1.0/badge.js(/cart/1.0.0/badge.js は版の文字列だけが違う)
export const version = '1.1.0';
export function render(el) { el.textContent = `cart ${version}`; }
<!-- index.html(ページ側) -->
<script type="importmap">
{ "imports": { "@team-cart/badge": "/cart/1.1.0/badge.js" } }
</script>
<div id="slot"></div>
<script type="module">
import { render } from '@team-cart/badge';
render(document.getElementById('slot'));
</script>
ページ側のコードは @team-cart/badge という名前しか知りません。import mapのURLを /cart/1.0.0/badge.js から /cart/1.1.0/badge.js へ書き換えるだけで、ページを再ビルドせずに表示が「cart 1.0.0」から「cart 1.1.0」へ切り替わりました。カート担当チームは新しい版を配信してimport mapの1行を更新すれば、次回のページ読み込みから新しい版を届けられます。これは独立デプロイの最小形であり、すでに開いている画面のモジュールを自動更新する仕組みではありません。
Next.js:nextjs-mfの廃止予定とMulti-Zones・Vercel Microfrontends
Module Federationの開発チームは2024年11月、告知で @module-federation/nextjs-mf を廃止(EOL)予定とし、Pages Routerには小さな修正だけを戻して新規開発は行わないと書きました。現状の機能が保たれるのは2026年中ごろから年末までの見込みで、2026年末ごろにはnextjs-mfのユニットテストをCIから外すとしています。2026年11月はこの期限の終わりにあたります。公式の対応表もNext.js 12〜15のPages Routerに限り、App Routerは非対応です。なお、Next.jsのアプリをViteで動かすVinextなら、@module-federation/vite でApp Routerのhostからremoteを読み込めると公式は案内しています。
App Routerのまま分割するなら、Next.js公式のMulti-Zonesが現実的です。/blog や /dashboard のようにパスごとに別のNext.jsアプリを同じドメインで配信し、各ゾーンは assetPrefix で静的ファイルの置き場を分けます。ゾーンの中の遷移はページを再読み込みしないソフトナビゲーション、ゾーンをまたぐ遷移は再読み込みを伴うハードナビゲーションになり、一緒に見られるページは同じゾーンに置くのが公式の指針です。別ゾーンへのリンクには next/link の Link ではなく <a> タグを使います。部品単位の実行時共有はできないので、垂直分割に限られます(Next.js自体の構成はNext.jsとは?16系App Routerで最小アプリを動かす手順と採用判断を参照)。
Vercelで配信しているなら、Vercel Microfrontendsがこのパス単位の振り分けをVercelのネットワーク側で行います。2025年10月31日に一般提供が始まり、設定は microfrontends.json に書きます。HobbyとProは2プロジェクトまで含まれ、Proで増やす場合は1プロジェクト月250ドル、振り分けのリクエストは100万件あたり2ドルです。フレームワークを問わず使えますが、Vercelへの依存が増える点は採用前に比べておきます。
導入後に起きやすい失敗:依存の重複・CSSの衝突・環境差
- 依存の重複:各アプリが自分のReactを抱えると、利用者はReactをアプリの数だけダウンロードします。Cam Jacksonの記事が欠点の1つ目に挙げる「ペイロードサイズ」の問題です。Module Federationなら共有指定で1つにまとめられますが、版をそろえる約束が必要になり、独立性と引き換えになります。
- CSSの衝突:グローバルなクラス名がぶつかると、詳細度が同じなら後から読み込んだアプリのスタイルが勝ちます。Shadow DOM、CSS Modules、アプリごとのクラス接頭辞のどれかで閉じ込め、リセットCSSはコンテナ側の1か所に限ります。
- 単体と統合時の挙動の差:記事は欠点の2つ目に「環境の違い」を挙げています。単体の開発サーバーでは動くのに、本番のコンテナに載せると、コンテナや他アプリが持ち込むグローバルなスタイルで崩れる問題です。記事の処方は、本番に近い環境へ定期的に統合・デプロイしてテストすることです。
- 運用の増加:リポジトリ、ビルドパイプライン、配信先、監視対象がアプリの数だけ増えます。記事は導入前の確認項目として、増えた基盤を自動で用意できるか、テストとリリースの手順がアプリの数に耐えられるかを挙げています。
既存モノリスからの段階移行の手順
一度に作り直さず、新しい仕組みを横に置いて機能を1つずつ移すストラングラーフィグパターンで進めます(切り戻しの設計はストラングラーフィグパターンとは?レガシー移行の実装手順と切り戻し設計を解説で扱っています)。
- 機能を棚卸しし、業務の区切りとURLの区切りが一致する機能を切り出し候補にする(垂直分割の候補)。
- ヘッダー・認証・ルーティングだけを持つコンテナ(アプリケーションシェル)か、パス単位の振り分け(Multi-Zones・エッジのルーティング)を用意し、既存モノリスをその中の1アプリとして載せる。
- 変更頻度が高く、他機能との状態共有が少ない機能を1つだけ別アプリにし、ルーティングで切り替える。
- 切り出したアプリ専用のビルドとデプロイのパイプラインを作り、モノリスと別の日に出せることを確かめる。
- 公開するモジュールやイベントの形を契約として固定し、契約テストとコンテナ込みのE2Eテストで新旧の組み合わせを検証する。
2つ目以降の機能は、1つ目で独立リリースが実際に回ってから切り出します。最初から全画面を分割すると、移行期間中に新旧2系統の保守が重なります。既存システムの刷新を移行計画から外部に任せる場合は、システムマイグレーション・リプレイスの支援もあります。
導入を見送るべき条件と判断の基準
マイクロフロントエンドが解くのは、技術ではなく組織の問題です。リリース日の調整や、他チームの変更待ちでデプロイが止まることが日常的に起きていないなら、導入しても運用の手間が増えるだけです。Vercelのドキュメントも、開発速度が目的ならモノレポ、フィーチャーフラグ、ビルドの高速化を先に検討するよう書いています。
- 見送る:フロントエンドを担当するチームが1つしかない。リリースの衝突が起きていない。全画面が同じ状態(ログイン情報以外も)を密に共有している。
- 先に試す代替:コードの境界が問題なら、1つのアプリの中でモジュール境界を守るモジュラモノリスで足ります。ビルド時間が問題なら、Turborepoなどのモノレポのキャッシュで縮められます。
- 導入を検討する:独立して出したいチームが複数あり、担当範囲がURLや画面の区切りと一致している。チームごとにフレームワークの更新時期をずらす必要がある。
導入を決めた場合も、フロントエンドを分けるのとバックエンドの境界をそろえるのは別の作業です。APIの合成を分担する設計はGraphQLフェデレーションの記事で扱っています。
よくある質問
マイクロフロントエンドとは何ですか?
1つのWebアプリの画面を業務の機能ごとに別チームが作り、別々にデプロイしたうえで、1つの製品として組み立てる設計手法です。マイクロサービスの考え方をフロントエンドに当てはめたもので、目的はチームごとの独立したリリースです。
マイクロサービスとの違いは何ですか?
マイクロサービスはサーバー側の処理をサービス単位に分ける設計で、マイクロフロントエンドはブラウザに届く画面を分ける設計です。両者は独立しており、バックエンドがモノリスのままフロントエンドだけを分けることもできます。
Module Federationとは何ですか?
別々にビルドしたアプリが、実行時に互いのモジュールを読み込み、共通の依存を1つにまとめる仕組みです。webpack 5で導入され、現在は @module-federation/enhanced がwebpackとRspack向けのプラグインを、@module-federation/vite がVite向けのプラグインを提供しています。
Next.jsのApp RouterでModule Federationは使えますか?
公式プラグインの @module-federation/nextjs-mf はPages Routerだけが対象で、App Routerには対応していません。プラグイン自体も廃止予定です。App Routerで分割するなら、パスごとに別アプリを配信するNext.jsのMulti-Zonesを使います。
小規模なチームでも導入すべきですか?
フロントエンドのチームが1つなら、導入を勧めません。リリースの衝突が起きていないところに分割を入れても、ビルドと配信の手間が増えるだけです。コードの境界を整えたいならモジュラモノリス、ビルド時間を縮めたいならモノレポのキャッシュから始めます。