LoginSignup
6

More than 1 year has passed since last update.

posted at

updated at

webpack 5 Module Federationにおけるchunk最適化

Module Federation概要

webpack 5でModule Federationという仕組みが入る。

例えば、以下のように書くと、app_01sharedオプションに記述したモジュールが、app_02のアプリに対して共有されるようになる。

plugins: [
  new ModuleFederationPlugin({
    name: "app_01",
    library: { type: "var", name: "app_01" },
    filename: "remoteEntry.js",
    remotes: {
      app_02: "app_02",
    },
    exposes: {
      SideNav: "./src/SideNav",
      Page: "./src/Page"
    },
    shared: ["react", "react-dom", "@material-ui/core", "react-router-dom"]
  }),
]
plugins: [
  new ModuleFederationPlugin({
    name: "app_02",
    library: { type: "var", name: "app_02" },
    filename: "remoteEntry.js",
    remotes: {
      app_03: "app_03",
    },
    exposes: {
      Dialog: "./src/Dialog",
      Tabs: "./src/Tabs"
    },
    shared: ["react", "react-dom", "@material-ui/core", "react-router-dom"]
  }),
]
<!DOCTYPE html>
<html lang="en">
  <head>
    <!-- app_02 -->
    <script src="http://localhost:3002/remoteEntry.js"></script>
  </head>
  <body>
    <div id="root"></div>
    <!-- app_01 -->
    <script src="http://localhost:3001/main.js"></script>
    <script src="http://localhost:3001/remoteEntry.js"></script>
  </body>
</html>

app_02のモジュールは非同期に読み込まれ、実行時にchunkを解決する。実行するまで不明なプログラムであるはずなので、エラーなどもその時までわからない。

しかし、逆に言えば、app_01app_02のビルドパイプラインは疎結合にできるので、Micro-Frontendsにおいて活用できそう、というわけだ。

overridablesとoverrides

ModuleFederationPluginでは、ContainerPluginContainerReferencePluginというクラスを内部で扱っている。詳細はこちらのとおりだが、それぞれがoverridablesoverridesというオブジェクトを提供している。

  • overridables - remoteとして読み込まれたとき、localによって上書き可能なモジュール
  • overrides - localとして読み込まれたとき、remoteを上書きするモジュール

これをハッシュで識別し、実行時に解決・最適化するのがModule Federationの実態だ。前述の例では、sharedオプションに指定されたモジュールが上書き対象となっている。

sharedの問題

しかし、sharedオプションをつかったchunkにはひとつ問題がある。

ModuleFederationPluginlocalremoteそれぞれのコードを生成する箇所は以下のようになっている。

compiler.hooks.afterPlugins.tap("ModuleFederationPlugin", () => {
  if (
    options.exposes &&
    (Array.isArray(options.exposes)
      ? options.exposes.length > 0
      : Object.keys(options.exposes).length > 0)
  ) {
    // remote 用のコード
    new ContainerPlugin({
      name: options.name,
      library: options.library || compiler.options.output.library,
      filename: options.filename,
      exposes: options.exposes,
      // options.shared その1
      overridables: merge(options.shared, options.overridables)
    }).apply(compiler);
  }
  if (
    options.remotes &&
    (Array.isArray(options.remotes)
      ? options.remotes.length > 0
      : Object.keys(options.remotes).length > 0)
  ) {
    // local 用のコード
    new ContainerReferencePlugin({
      remoteType:
        options.remoteType ||
        (options.library && options.library.type) ||
        compiler.options.externalsType,
      remotes: options.remotes,
      // options.shared その2
      overrides: merge(options.shared, options.overrides)
    }).apply(compiler);
  }
});

これをみると、options.sharedは2箇所でつかわれていることがわかる。つまり、sharedを使うと、overridesoverridablesを同時に提供してしまうので、アプリのユースケースよっては不要なコードが生成されてしまうのだ。

具体的なパターンをみてゆく。

overridablesのみ提供したいパターン

例えば、app_02を以下のように仮定する。

  • app_02remoteとして参照されることがあり、いくつかのライブラリをlocal(app_01)に上書きしてほしい
  • app_02からremote(app_03)を呼ぶケースでは、app_02remoteで共通するライブラリがない
  • この場合「app_02overridablesのみを提供し、overridesを提供することはない」ということになる。

sharedを使うと、local用のビルドでoverrideschunkもつくってしまう。しかし、前述のような想定だと、overrideschunkが使用されるケースはない。

これを解決するのがoverridablesオプションである。sharedの代わりにoverridablesと明示的に指定することで、overridesが生成されることはなくなる。

plugins: [
  new ModuleFederationPlugin({
    name: "app_02",
    library: { type: "var", name: "app_02" },
    filename: "remoteEntry.js",
    remotes: {
      app_03: "app_03",
    },
    exposes: {
      Dialog: "./src/Dialog",
      Tabs: "./src/Tabs"
    },
    // 変更箇所
    overridables: ["react", "react-dom", "@material-ui/core", "react-router-dom"]
  }),
]

実際には、以下のようにchunkを削除できる。

スクリーンショット 2020-05-28 18.16.18.png

overridesのみ提供したいパターン

同様に、例えば、app_01を以下のように仮定してみる。

  • app_01app_02remoteとして参照し、いくつかのライブラリを上書きしたい
  • app_01はライブラリのバージョンが古いため、remoteとして参照される場合に、ライブラリを上書きされたくない
  • この場合「app_01overridesのみを提供し、overridablesを提供することはない」ということになる。

shredを使うと、remote用のビルドでoverridableschunkをつくってしまう。前述のような想定だと、overridableschunkが使用されることがないし、また、生成されるべきでもない。

これはoverridesオプションで解決できる。sharedの代わりにoverridesを使うことで、overridablesが生成されることはなくなる。

plugins: [
  new ModuleFederationPlugin({
    name: "app_01",
    library: { type: "var", name: "app_01" },
    filename: "remoteEntry.js",
    remotes: {
      app_02: "app_02",
    },
    exposes: {
      SideNav: "./src/SideNav",
      Page: "./src/Page"
    },
    // 変更箇所
    overrides: ["react", "react-dom", "@material-ui/core", "react-router-dom"]
  }),
]

実際には、以下のようにchunkを削除できる。

スクリーンショット 2020-05-28 18.18.23.png

雑感

  • 実際はこれらを手で管理してゆくのは厳しくないか・・?
  • アプリケーションのユースケースと責務がはっきりしていれば、明示的に指定することはできるかもしれない
  • 最初から意識しておけばsharedを使うことこと自体は減りそう
  • automatic-vendor-federationというプラグインがあるが、現状はsharedしか対応していない
  • chunkが生成されてしまうだけで、fallbackもあるのでバグるわけではない。あくまで最適化したい場合
  • 脳死sharedでもそんなには困らないのかも

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
6