Differential Serving で必要なコードは 2 割以上削れる！

> Differential Serving と言う技術は実はもう２年ほど前から[Philip Walton さんの記事](https://philipwalton.com/articles/deploying-es2015-code-in-production-today/)をきっかけに知る人ぞ知る JS のバンドルサイズを減らせるための技術なんですけど、検索しても、日本語でこのテーマについて話す記事はないので、とりあえずまとめ記事のつもりでこれを書きました、よろしくお願いします！

# 何のための技術ですか？

今でも IE 11 のサポートを切れないアプリは多いと思います、そう言うアプリを作っている人はほとんど IE 11 を含めた全てのブラウザーにアプリがちゃんと起動できるように ES5 までにコードをコンパイルしていると思います。

しかし、IE 11 のために追加する polyfills や ES5 でコードを書くとどうしても必要になって来る追加コードはビルドされたバンドルを重くします。

どれぐらい重くなるかと言うのはコード次第ですが大体 20%から 30%までになります。

> [Shubham Kanodia さんがこの記事](https://www.smashingmagazine.com/2018/10/smart-bundling-legacy-code-browsers/)でいくつかのライブラリーをテストしました

しかも、ユーザー全体の[ほぼ 90%が](https://caniuse.com/#feat=es6-module) ES Modules に対応しています!　つまり、10%のユーザーのために 90%のユーザーに送るコードの量が多くなっています。

それでもその 10%のユーザーを切れないアプリは多いと思います、なんかこう都合良く ES Modules に対応しているユーザーだけにその小さめのコードを送れるためのフロントエンドコードはないのかな…

実はあります、これからそれを紹介します。

# もの凄く簡単に言うとこれです

```html
<!-- 主にIE 11のためのJS -->
<script nomodule src="/app.legacy.js"></script>
<!-- 最新ブラウザーのためのJS -->
<script type="module" src="/app.js"></script>
```

コンセプト的にはアプリのバンドルを二つ用意します。

1. IE11 などの legacy ブラウザー用の ES5 にコンパイルされたバンドル。
2. 最新ブラウザー用の ES Modules にコンパイルされたバンドル。

ここで重要なのは `nomodule` と `type="module"` です、これらを使ってユーザーのブラウザーにどのコードをロードするかの判断を任せます。

# `nomodule`と`type="module"`って何ですか？　

`nomodule`は`script`タグの属性で ES Modules に対応しているブラウザーにこのコードを無視するように示します。

`type="module"`は逆にブラウザーにコードは ES Modules で書いてあることを示します。

これらを合わせたサンプルは[これです](https://glitch.com/~alangdm-differential-serving)。

このコードを ES Modules に対応しているブラウザーで見るとこうなります。

<img style="width: 100%;" alt="Modernブラウザーの例" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537488/6501a13b-d8a3-131c-2b14-82efe0f811ff.png">

見ての通り`type="module"`になっているコードだけをダウンロードして実行します。

そして、こちらのサンプルを IE 11 で見るとこうなります。

<img alt="Legacyブラウザーの例" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/537488/95b3bfd3-58f5-f7c3-c664-8a461de0c5e7.png">

見ての通り`nomodule`になっているコードだけを実行するけど両方をダウンロードしていますね…

# そこまで簡単には行かないんですよね

先ほどの例もそうですけど、実は Safari 10 や Firefox 58 や IE 11 や Edge 15-18 などは両方のコードをダウンロードします。

でもこれらはほとんど Wi-Fi でしか使わない PC ブラウザーだからそこまで問題にはならないと思われがちですがタチの悪いことに Safari 10 は両方をダウンロードするだけでなく両方を実行します…

> John Stew さんは[こちらでいくつかのテストを行ったので詳しくはこれを見てください](https://github.com/johnstew/differential-serving)。

Safari 10 のこの記事を書いている時点でのユーザーの割合は 1%以下ではありますができるだけ多くのユーザーに対応したいのでこれだけだとアウトなんですよね…

# じゃもう手詰まり？

実は上記のブラウザーの問題を解決できる方法はあります、しかも、それを都合良くまとめてくれる方法は色々な人気のツールにはすでに入っています。

# ツールは何をしていますか？

ツールはこう言ったコードを自動的に書いてくれる。

```html
<!-- これが実際にVue CLIを使うと出て来るコードとほぼ同じものです -->
<script type="module" src="https://example.com/app.js"></script>
<script>
  !(function() {
    var check = document.createElement("script");
    if (!("noModule" in check) && "onbeforeload" in check) {
      var support = false;
      document.addEventListener(
        "beforeload",
        function(e) {
          if (e.target === check) {
            support = true;
          } else if (!e.target.hasAttribute("nomodule") || !support) {
            return;
          }
          e.preventDefault();
        },
        true
      );

      check.type = "module";
      check.src = ".";
      document.head.appendChild(check);
      check.remove();
    }
  })();
</script>
<script
  type="text/javascript"
  src="https://example.com/app.legacy.js"
  nomodule
></script>
```

これは利用できる範囲の JS の feature の中で Safari 10 を見つけてその場合に module と nomodule の script が両方実行されないように働きかけます。

> 詳しくは[こちらの gist](https://gist.github.com/samthor/64b114e4a4f539915a95b91ffd340acc)を見てください。

これはあくまで一つのやり方で、こちらではまだ legacy ブラウザーが両方ダウンロードすることになります、これから紹介するツールの中で、それすら解決するものもあるので、自分のプロジェクトに合うものもあると思います。

## Vue CLI 3+の場合

Vue CLI を使っているなら話はすごい簡単です、ビルド段階で[フラグを一つ追加するだけで済みます](https://cli.vuejs.org/guide/browser-compatibility.html#modern-mode)。

```bash
vue-cli-service build --modern
```

## webpack の場合

webpack を使っているならもう少し設定をいじる必要はあります。

1. この 2 つのプラグインの内から 1 つをインストールします。
   - [html-webpack-multi-build-plugin](https://www.npmjs.com/package/html-webpack-multi-build-plugin)
   - [webpack-module-nomodule-plugin](https://www.npmjs.com/package/webpack-module-nomodule-plugin)
2. 次に webpack で ES5 用の設定と ES Modules 用の設定を用意します。[こちらの記事](https://dev.to/thejohnstew/differential-serving-3dkf#generate-two-bundles)の「Generate two bundles」の部分にいい例があります。

3. ➁ でビルドしたバンドルを ➀ のプラグインに入れます

## Rollup の場合

Rollup では[rollup-plugin-index-html](https://www.npmjs.com/package/rollup-plugin-index-html)を使えば簡単にできます。

こちらは性能が良く、設定次第では両方がダウンロードされてしまう問題まで解決できるようになっています。

使い方は先ほどの webpack の使い方に似ています。

Web Components でプロジェクトを作っているなら、先ほどのプラグインを内部で使っている[Open WC の設定](https://open-wc.org/building/rollup-plugin-index-html.html#multi-legacy-and-modern-build) がおすすめです。

Web Components のプロジェクトでなくても上記の設定が参考になると思います。

# 最後に

Differential Serving は個人的に誰もが知っておくべき技術だと思います、これを入れるとまだ legacy ブラウザーを使っているユーザーを犠牲にせず最新ブラウザーを使っているユーザーにもっといい体験をさせられます。いわゆる「Win Win」な話です。

# もっと知りたい人には（全て英語の記事ですみません mm）

- https://philipwalton.com/articles/deploying-es2015-code-in-production-today/
- https://github.com/johnstew/differential-serving
- https://www.smashingmagazine.com/2018/10/smart-bundling-legacy-code-browsers/
- https://css-tricks.com/differential-serving/
- https://dev.to/thejohnstew/differential-serving-3dkf