※ こちらはミツモアAdvent Calendar 2021の24日目の記事です。
メリークリスマス!ミツモアの@teradonburiです。皆さんフロントエンド開発バリバリしてますか?弊社ではフロントエンドのフレームワークにReactを採用し、プロダクトを開発し始めてから早4年近くの歳月が経ちました。4年も経つとフロントエンドの行数も膨大な量となっていきます。計測してみた所フロントエンドの実装だけで約28万行(.tsx 252159行+.ts 32979行)もの巨大コード量になっていました。(2021/12/01調べ)bundleのサイズとしては実に16MB近くのファイルサイズになっておりました。
弊社ではモジュールバンドラとしてwebpackを採用しております。SPAフロントエンドの高速化手法としてSSG化、CDN、prefetchなどのキャッシュや不要npmパッケージの削除やTree Sharkingなどのbundle自体の軽量化、動的importなどの遅延ロードに実行時の最適化などなどが挙げられますが、今回は巨大なbundleの中身を複数のアプリケーションに分割し、それらを統合した1つのアプリケーションを作成する方法を紹介します。俗にマイクロフロントエンドと呼ばれているものです。
1つの巨大フロントエンドアプリケーションであることの問題点
いくつかありますが
- 行数に比例してビルドの時間が長くなる。(修正無関係のファイル含めて都度全体ビルドしなければならない)
- ビルド後のbundle.jsは一つの巨大な塊のファイルのため、初回のファイル読み込みが遅い。および初回JSのレンダリング実行時間が長くなる。(仮想DOMツリーの生成などもあるため)
2に関してはミツモアではwebpackのマジックコメントによるCode Splitingとloadble-componentsによる遅延ロードを行っており、解決済みです。他にもCloudFrontでのCDNによるキャッシュにより高速化されております。
増大するビルド時間の改善に関してはwebpackからesbuildに乗り換えるなどの案も一度出ましたが、載せ替えが困難なこととES5やdynamic importによる遅延ロードやHMRに対応していなく、まだ本番利用にも耐えきれなさそうな感じがしたので見送りになりました。
そもそもの原因である1は機能追加や画面追加などで実装が肥大化していく以上ネックとなっていく壁なため、1つのbundleにビルドするという方法では根本解決することはできません。そこで考えられるのはbundle自体を分割、つまりフロントエンドアプリケーションを分割し、それらを組み合わせて1つのアプリケーションを作るという手段です。
この手法はマイクロフロントエンドと呼ばれています。
マイクロフロントエンドの利点はそれぞれビルドされるため、並列ビルドおよびそれぞれのアプリケーションを独立して動作確認することができます。webpack 5のModuleFederationPluginを使うことで個別にビルドされたbundleファイルのモジュールやコンポーネント等を参照することができます。これにより1つのページで複数のアプリケーションが統合されたマイクロフロントエンドを実現することが可能となります。
そう、webpack 5のModule Fedarationならね。
Webpack 5で始めるマイクロフロントエンド入門(サンプル)
こちらの記事を参考に簡単なwebpack+Reactのマイクロフロントエンドアプリケーションのサンプルを作ってみようと思います。ちなみにcreate-react-appは使いません。
今回のサンプルは次のgithubリポジトリに置いてあります。github.com
今回のアプリケーションのフォルダ構成です。(Mono repository)headerとmain二つのアプリケーションを作成し、main側からはheaderアプリケーションを参照するようにします。
headerアプリケーションを作成します。src/header/index.tsxはbootstrap.tsxを参照するだけのファイルです。
後述のModuleFederationPluginのsharedで参照するReactライブラリを同期待ちするため、アプリケーションそのものを動的importでロードします。
src/header/bootstrap.tsxは変哲もないReactアプリケーションです。
src/header/Header.tsxはヘッダーを表示するコンポーネントです。
headerアプリケーション用のビルド設定をwebpack.header.jsに書きます。
ModuleFederationPluginのnameプロパティにはチャンク名を指定します。filenameは分割アプリケーションとして出力するファイル名です。exposesには出力対象のファイルを指定します。sharedにはアプリケーション間で共通のライブラリを指定することでbundleサイズを節約できます。共通ライブラリの読み込み完了待ちする必要があるため、enrtyポイントで動的importする必要があります。
HtmlWebpackPluginはserveで動作する際、index.htmlをコピーし、bundle-header.jsをscriptタグを埋め込みます。ただし、この際ModuleFederationPluginで出力されたheader.jsも埋め込まれてしまうため、excludeChunksでheader_appのチャンクを除外することでheader.jsを除外し、bundle-header.jsと二重にスクリプトが埋め込まれるのを防ぎます。
package.jsonのscriptsで定義したwebpack serveコマンドでビルドしローカルサーバ起動します。
localhost:3002にヘッダーアプリケーションが立ち上がります。
単独でheaderアプリケーションが動くのが確認できました。
続いて、mainアプリーケーションを作成します。src/main/index.tsxに同様にbootstrap.tsxを参照するentryポイントを作成します。
bootstrap.tsxは同様に単純なReactアプリケーションを作成しているのみです。
Main.tsxでheaderアプリケーションからHeaderコンポーネントを参照します。
型参照をするためにtsconfig.jsonのpathsに(チャンク名)/(exposes対象)のpathsを追加します。
webpack.main.jsでModuleFederationPluginを追加します。
remotesプロパティに(チャンク名@ホストパス)を指定します。productionビルド時はホスティングしているurlに合わせて適宜変える必要があります。sharedには同じpackage.jsonから参照したreact, react-domのバージョンを指定しています。
package.jsonのscriptsで定義したwebpack serveコマンドでビルドしローカルサーバ起動します。
localhost:3001にmainアプリケーションが立ち上がります。
headerアプリケーションモジュールを含んだアプリケーションを実行できました。
まとめ
以上、webpack 5でのマイクロフロントエンド入門に関して紹介しました。今回はやりませんでしたが、WebConponent化すればReactやVueなどのクロスフレームワークを統合したmainアプリケーションを作成することも可能です。
ミツモアの開発部では、エンジニアがプロダクトの磨き上げに集中できるような環境を整えられるよう、日々新たなツールや技術の検討を行なっております。
現在、事業拡大を進めておりエンジニア・デザイナー・PdMを積極採用中です。ぜひWantedlyのリンクからカジュアル面談をしましょう。