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


- ※これからかくことは次回リリースで変更される可能性もあります
- ※こちらのPRでoverrides/overridableオプションは廃止され、sharedオプションで調整できるようになりました (追記@2020/06/02)
    - [Versioned shared modules for Module Federation by sokra · Pull Request #10960 · webpack/webpack](https://github.com/webpack/webpack/pull/10960)


## Module Federation概要

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

- [webpack 5 の webpack federation という概念について - Qiita](https://qiita.com/mizchi/items/f335d3e37395dbffa593)
- [webpack@5で入るModule Federationについて - 技術探し](https://blog.hiroppy.me/entry/module-federation)

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

```js
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"]
  }),
]
```

```js
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"]
  }),
]
```

```html
<!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_01`と`app_02`のビルドパイプラインは疎結合にできるので、Micro-Frontendsにおいて活用できそう、というわけだ。

## overridablesとoverrides

`ModuleFederationPlugin`では、`ContainerPlugin`、`ContainerReferencePlugin`というクラスを内部で扱っている。詳細は[こちら](https://webpack.js.org/concepts/module-federation/#building-blocks)のとおりだが、それぞれが`overridables`、`overrides`というオブジェクトを提供している。

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

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

## sharedの問題

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

`ModuleFederationPlugin`で`local`と`remote`それぞれのコードを生成する箇所は以下のようになっている。

```js
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`を使うと、`overrides`と`overridables`を同時に提供してしまうので、アプリのユースケースよっては不要なコードが生成されてしまうのだ。

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

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

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

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

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

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

```js
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を削除できる。

<img width="1239" alt="スクリーンショット 2020-05-28 18.16.18.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22875/f40a2c4f-1092-25b0-ecd0-299a87d38a47.png">


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

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

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

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

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

```js
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を削除できる。

<img width="1230" alt="スクリーンショット 2020-05-28 18.18.23.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/22875/8d5a02f8-e32e-ebb0-6f32-99f1b4f06453.png">


## 雑感

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


## 参考

- [Remove overrides/overridables option for ModuleFederationPlugin by okmttdhr · Pull Request #10954 · webpack/webpack](https://github.com/webpack/webpack/pull/10954)
- [Webpack 5 Module Federation in 3 minutes - DEV Community 👩‍💻👨‍💻](https://dev.to/okmttdhr/webpack-module-federation-in-3-minutes-4d3l)