Babel が行っている Polyfill の仕組みについて

本記事は リクルートライフスタイル Advent Calendar 2019 8 日目の記事です。

先日プレスリリースが出ましたホットペッパービューティーの美容クリニック検索・予約サービスでデザインとフロントエンドを担当している手塚です。

この記事では Babel がどのように Polyfill を行っているのかを紹介します。

まずは新しい言語仕様を利用するために Babel が解決している問題とそのアプローチを下表に示します。

問題 アプローチ
ブラウザが class 構文やアロー関数、テンプレートリテラルなどの新しい構文を理解できない ES2015 以上の言語仕様で書かれた構文を ES5 の構文に変換
実行ブラウザによって JavaScript エンジンとバージョンに差分があり利用できる機能が違う ブラウザがサポートしていない機能を Polyfill で互換実装

アプローチの 1 つである Polyfill による互換実装は、もうアップデートされない IE11 の対応や言語仕様を追従し続ける各ブラウザの実装差分をサポートするとても便利なアプローチなのですが、ブラウザに代わり自ら互換実装を行うため副作用が伴うこともあります。

副作用によって不具合が発生した場合、どのように互換実装をしているのかを理解していないと解決が困難な場合もあるので、 Babel の基本的な処理を追いつつ Polyfill について説明していきます。

なお、この記事の内容は以下のバージョンを元に記述しています。そのため Babel 7.4.0 で非推奨となった @babel/polyfill には触れません。

  • Babel 7.7.4
  • core-js 3.4.7

Babel の基本的な処理の流れ

まずは Babel を利用した Polyfill による互換実装を簡単に追ってみましょう。

  1. 開発対象ブラウザの設定
  2. 開発対象ブラウザ上で動作するように構文変換・ Polyfill の注入
  3. 生成されたファイル実行時の Polyfill による不足機能の互換実装処理後、メイン処理を実行

1 と 2 が事前処理、 3 が実行時の処理となります。 新しい言語仕様がどこまで利用可能かは、変換したファイルが実行されたブラウザのJavaScriptエンジンの種類とバージョン次第となるため、設定された開発対象ブラウザに応じた構文変換と Polyfill の注入を事前処理とし、 Polyfill による互換実装を変換したファイル実行時の処理としています。

Babel はこのように事前処理/実行時処理というアプローチをとっていますが、 Polyfill.io のように実行時に User Agent をチェックして必要な Polyfill を返すというアプローチを行っているサービスもあります。

開発対象ブラウザの設定と Polyfill の注入

1 と 2 の事前処理の動作を確認するために最低限の設定を行ったサンプルを用意しました。サンプルでは @babel/preset-env の useBuiltIns オプションは usage を選択し、変換元ファイルの内容に必要な Polyfill のみを注入する設定で、この後の説明を進めていきます。

Polyfill の選定と注入は @babel/preset-env プラグイン が担当します。

まず開発対象ブラウザを @babel/preset-env プラグインのオプションとして .babelrc などの config ファイル内に記述します。

1
2
3
"targets": {
  "ie": "11"
},
1
2
3
4
// browserlist  query 形式で指定することも可能
"targets": {
  "browsers": ["ie 11", "last 2 chrome version"]
},

ここで指定された開発対象ブラウザと core-js-compat が提供する 各ブラウザの新しい言語仕様の実装状況 と照らし合わせて必要な Polyfill を注入します。

@babel/preset-env 内で core-js-compat が参照されている箇所

では、 IE11 に実装されていない includes() メソッド を埋め込んだコードにどのように Polyfill が注入されるかを見てみましょう。

1
[1, 2, 3].includes(2);

サンプルのタスクを使って Babel で変換してみると、次のように1行目に Polyfill が注入されます。 (次の例では import 構文が使われていますが、この記事ではモジュール形式については語りません。)

1
2
import "core-js/modules/es.array.includes";
[1, 2, 3].includes(2);

※ 注入されたソースはこちら で確認できます。

続いて .babelrc で指定している対象ブラウザを Chrome だけにして Babel で変換してみましょう。

1
2
3
"targets": {
  "browsers": ["last 2 chrome version"]
},

IE11 が対象ブラウザから外れ、 IE11 のために Polyfill を注入する必要がなくなったので、変換前のコードがそのまま出力されます。

1
[1, 2, 3].includes(2);

このように @babel/preset-env に設定された対象ブラウザと各ブラウザの互換情報がまとまった core-js-compat と照らし合わせて、必要な Polyfill を取り込んでいることがわかりました。これで Polyfill がうまく注入されない場合に「対象ブラウザの指定が悪いのかな?」「 core-js-compat のデータが古いのかな?」などの仮説を持って確認できます。

ちなみに ECMAScript は 2015 年から毎年改定されることになりました。 ECMAScript が毎年改定されるということは、必然的に各ブラウザも追従していくことになります。したがって 各ブラウザの言語仕様のキャッチアップ状況をまとめた core-js-compat は、それぞれのブラウザの更新状況をウォッチし、常にアップデートされていくこととなります。 commit 頻度を見てみると毎日のようにアップデートされているので、 core-js-compat は出来る限り最新の状態に保っておくことが望ましいでしょう。

Polyfill による不足機能の互換実装

先の Polyfill の注入例から対象ファイルの先頭に Polyfill が注入されるようになることがわかりましたが、 Polyfill を利用する場合にメイン処理より先に Polyfill を実行して、不足機能の互換実装をしてからメイン処理を実行する、という制約が生まれます。

出力するファイルが 1 ファイルしかない場合、この制約はあまり気にならないのですが、 何かしらの理由で複数ファイルの運用が必要になった場合に、この制約が大きな影響力を持ちます。

その場合 webpack などのモジュールバンドラーを使って依存関係を解決することで、スクリプトの順番を考慮する必要が少なくなるためモジュールバンドラーを利用することがよいのではないでしょうか。

Babel が Polyfill を注入するスコープ

クライアントサイドの JavaScript 開発する場合 Ecma International が策定する ECMAScript と WHATWG / W3C が策定する Web API を扱うことになりますが、 Babel の Polyfill 対象となるのは JavaScript コア言語と一部の Web API で、大部分の Web API は対象ではありません。

@babel/preset-env が Polyfill に利用する core-js も Polyfill の提供対象を「 ECMAScript 2020 までの言語仕様と Promise や Symbols 、iterators 、 ECMAScript proposals 、 WHATWG / W3C が策定するいくつかの機能」としています。(2019年12月5日時点)

したがって Babel の Polyfill 対象ではない Web API に関する新しい仕様を取り込みたい場合は、 npm や GitHub に公開されている Polyfill の中から最適なものを探すか、自分で Polyfill を実装して取り込む必要があります。

※ JavaScript コア言語は ECMAScript として策定されている言語構文を指し、 Web API は DOM API などを指します。これらは MDN の JavaScript 技術概説が参考になるので、そちらをご覧ください。

まとめ

  • Babel は core-js-compat という、各ブラウザの新しい言語仕様の実装状況を参考に対象ブラウザに必要な Polyfill を注入する。 Polyfill 対象が正しいかどうかは core-js-compat によって管理されている。
  • Polyfill を利用する場合、 Polyfill を実行して互換実装処理を終えてからメイン処理を実行する必要が生まれるため依存関係が生まれる。
  • Babel によって Polyfill を注入できるのは JavaScript コア言語と一部の Web API のみ。 Polyfill 対象ではない Web API の Polyfill を利用したい場合、npm や GitHub に公開されている Polyfill の中から最適なものを探すか、自分で Polyfill を実装して取り込む必要がある。

Babel が Polyfill を注入する仕組みをあらためてみてみると、 core-js が Polyfill の実装を用意して core-js-compat が愚直に Polyfill 対象を管理し、開発対象のブラウザに必要な Polyfill を core-js-compat と照らし合わせて 1 つずつ注入していくことで、ブラウザの互換実装を実現するものでした。

このことから Polyfill 自体は、決してスマートなものではなく泥臭く 1 つ 1 つの課題を解決していることがわかります。よって「 Polyfill を使えば安心」ではなく、この仕組みを正しく利用するために 最新の core-js を利用する などを意識する必要があります。

今 Babel を通して Polyfill を利用している方は Babel や @babel/preset-env 、 core-js などのバージョン確認だけでもしてみてはいかがでしょうか? ☺️

※ Babel 7.4.0 未満を利用している方は @babel/babel-polyfill のバージョン確認が必要です。

この記事が少しでも Babel が行う Polyfill 処理の仕組みを理解する手助けとなれば幸いです。