LoginSignup
6
6

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-05-28

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でもそんなには困らないのかも

参考

6
6
0

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
  3. You can use dark theme
What you can do with signing up
6
6