Differential Serving と言う技術は実はもう2年ほど前からPhilip Walton さんの記事をきっかけに知る人ぞ知る JS のバンドルサイズを減らせるための技術なんですけど、検索しても、日本語でこのテーマについて話す記事はないので、とりあえずまとめ記事のつもりでこれを書きました、よろしくお願いします!
何のための技術ですか?
今でも IE 11 のサポートを切れないアプリは多いと思います、そう言うアプリを作っている人はほとんど IE 11 を含めた全てのブラウザーにアプリがちゃんと起動できるように ES5 までにコードをコンパイルしていると思います。
しかし、IE 11 のために追加する polyfills や ES5 でコードを書くとどうしても必要になって来る追加コードはビルドされたバンドルを重くします。
どれぐらい重くなるかと言うのはコード次第ですが大体 20%から 30%までになります。
Shubham Kanodia さんがこの記事でいくつかのライブラリーをテストしました
しかも、ユーザー全体のほぼ 90%が ES Modules に対応しています! つまり、10%のユーザーのために 90%のユーザーに送るコードの量が多くなっています。
それでもその 10%のユーザーを切れないアプリは多いと思います、なんかこう都合良く ES Modules に対応しているユーザーだけにその小さめのコードを送れるためのフロントエンドコードはないのかな…
実はあります、これからそれを紹介します。
もの凄く簡単に言うとこれです
<!-- 主にIE 11のためのJS -->
<script nomodule src="/app.legacy.js"></script>
<!-- 最新ブラウザーのためのJS -->
<script type="module" src="/app.js"></script>
コンセプト的にはアプリのバンドルを二つ用意します。
- IE11 などの legacy ブラウザー用の ES5 にコンパイルされたバンドル。
- 最新ブラウザー用の ES Modules にコンパイルされたバンドル。
ここで重要なのは nomodule
と type="module"
です、これらを使ってユーザーのブラウザーにどのコードをロードするかの判断を任せます。
nomodule
とtype="module"
って何ですか?
nomodule
はscript
タグの属性で ES Modules に対応しているブラウザーにこのコードを無視するように示します。
type="module"
は逆にブラウザーにコードは ES Modules で書いてあることを示します。
これらを合わせたサンプルはこれです。
このコードを ES Modules に対応しているブラウザーで見るとこうなります。
見ての通りtype="module"
になっているコードだけをダウンロードして実行します。
そして、こちらのサンプルを IE 11 で見るとこうなります。
見ての通りnomodule
になっているコードだけを実行するけど両方をダウンロードしていますね…
そこまで簡単には行かないんですよね
先ほどの例もそうですけど、実は Safari 10 や Firefox 58 や IE 11 や Edge 15-18 などは両方のコードをダウンロードします。
でもこれらはほとんど Wi-Fi でしか使わない PC ブラウザーだからそこまで問題にはならないと思われがちですがタチの悪いことに Safari 10 は両方をダウンロードするだけでなく両方を実行します…
John Stew さんはこちらでいくつかのテストを行ったので詳しくはこれを見てください。
Safari 10 のこの記事を書いている時点でのユーザーの割合は 1%以下ではありますができるだけ多くのユーザーに対応したいのでこれだけだとアウトなんですよね…
じゃもう手詰まり?
実は上記のブラウザーの問題を解決できる方法はあります、しかも、それを都合良くまとめてくれる方法は色々な人気のツールにはすでに入っています。
ツールは何をしていますか?
ツールはこう言ったコードを自動的に書いてくれる。
<!-- これが実際に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を見てください。
これはあくまで一つのやり方で、こちらではまだ legacy ブラウザーが両方ダウンロードすることになります、これから紹介するツールの中で、それすら解決するものもあるので、自分のプロジェクトに合うものもあると思います。
Vue CLI 3+の場合
Vue CLI を使っているなら話はすごい簡単です、ビルド段階でフラグを一つ追加するだけで済みます。
vue-cli-service build --modern
webpack の場合
webpack を使っているならもう少し設定をいじる必要はあります。
-
この 2 つのプラグインの内から 1 つをインストールします。
-
次に webpack で ES5 用の設定と ES Modules 用の設定を用意します。こちらの記事の「Generate two bundles」の部分にいい例があります。
-
➁ でビルドしたバンドルを ➀ のプラグインに入れます
Rollup の場合
Rollup ではrollup-plugin-index-htmlを使えば簡単にできます。
こちらは性能が良く、設定次第では両方がダウンロードされてしまう問題まで解決できるようになっています。
使い方は先ほどの webpack の使い方に似ています。
Web Components でプロジェクトを作っているなら、先ほどのプラグインを内部で使っているOpen WC の設定 がおすすめです。
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