Webフロントエンドのpolyfillをbabelの利用・core-jsのダイレクト利用・swcの利用など、複数方式での比較を行ってみた

## はじめに

JavaScriptにおけるpolyfill(ポリフィル)とは、例えば `Promise` オブジェクトが使えないIE11でPromiseを使えるようにするなど、使いたいAPI(オブジェクト, メソッド等)がない環境にそれらを使えるようにする役目を果たすライブラリ及びその機構のことです。

polyfillをしたことの無い人にとっては、どのライブラリがどんな役目を持っていて、自分が望むpolyfillを実現するためのconfigの構築は難しいものと思っています。

そこで、polyfillを実現する複数の方法において、どのようなケースでどのような機構を使ってどのようなconfigを作れば良いのか、以下書き連ねておきます。

## 想定読者

* babelを噛ませてpolyfillさせたい人
* core-jsを直接読み込ませてpolyfillさせたい人
* swcを使ってpolyfillさせたい人

## polyfillを実現するためのconfig一覧

以下の私のリポジトリで検証したものをピックアップしていきます。

https://github.com/shoutatani/polyfill-playground

### babelを使ってpolyfillを実現する

https://github.com/shoutatani/polyfill-playground/tree/main/polyfill-babel-js

まずはbabel(ES2015以降のJavaScriptをES5に変換するツールおよびそのライブラリ群)を通してpolyfillを実現する方法です。
ES2015以降のJavaScriptをメインのコードベースとして構築している環境には、babelを使っている環境もそれなりにあるのではないでしょうか。

babelを通してpolyfillを注入する方法として、主に三つの方法があります。

1. [@babel/preset-env](https://babeljs.io/docs/en/babel-preset-env) を使う方法
1. [@babel/plugin-transform-runtime](https://babeljs.io/docs/en/babel-plugin-transform-runtime) を使う方法
1. [babel-plugin-polyfill-corejs3](https://www.npmjs.com/package/babel-plugin-polyfill-corejs3) を使う方法

どれを選べば良いのか。私の考えとしては以下のとおりです。

* ライブラリ作者であれば、 `babel-plugin-polyfill-corejs3` を使って、`usage-pure` の設定でpolyfillする
* ライブラリではないアプリケーションの開発者であれば、 `@babel/preset-env` を使って、 `entry` もしくは `usage` の設定でpolyfillする

#### ライブラリ作者であれば、 `babel-plugin-polyfill-corejs3` を使って、`usage-pure` の設定でpolyfillする

`babel-plugin-polyfill-corejs3` というbabelプラグインを使ってpolyfillする方法です。

https://www.npmjs.com/package/babel-plugin-polyfill-corejs3

webpackを利用している場合の、babel-loader込みの設定例は次の通りです。
(`babel-loader`の他、`@babel/core`、`babel-plugin-polyfill-corejs3`、`core-js` がpackage.jsonに追加されている必要があります)

```webpack.config.js
const path = require("path");

module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist/usage-pure-corejs3"),
  },
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            targets: {
              ie: "11",
            },
            plugins: [
              ["polyfill-corejs3", { method: "usage-pure", version: "3.23" }],
            ],
          },
        },
      },
    ],
  },
};
```

`@babel/plugin-transform-runtime` を使ってpolyfillする場合には、targets(どの実行環境に向けてトランスパイルするか)に関わらず全てのpolyfill可能な対象がpolyfillされてしまいますが、この `babel-plugin-polyfill-corejs3` プラグインを利用した場合には、targetsを見てpolyfill可能です。

https://github.com/babel/babel-polyfills#supported-polyfills

上のリンクを見れば分かる通り、 `babel-plugin-polyfill-corejs3` には複数のpolyfill設定が可能となっています。

1. `entry-global` 
    * targetsを見て、targetsにおいて不足しているpolyfillを注入するモード。
    * polyfillの副作用がglobalに及ぶ。
1. `usage-global`
    * targetsだけじゃなく、実際に使用しているコードを見て、必要なだけのpolyfillを注入するモード。
    * polyfillの副作用がglobalに及ぶ。
1. `usage-pure`
    * targetsだけじゃなく、実際に使用しているコードを見て、必要なだけのpolyfillを注入するモード。
    * polyfillの副作用がglobalに及ばず、`Promise` などの呼び出しが独自の関数呼び出しに置き換えることでpolyfillを実現している。

先のwebpackのコード例は、先述の `usage-pure` の例です。
ライブラリという様式上、その副作用をライブラリ使用側に及ぼさないためには`usage-pure`を利用するのが適しているのではないでしょうか。

なお、`babel-plugin-polyfill-corejs3` プラグインのコアはcore-js(JavaScriptでpolyfillを行うコアライブラリの一つ。)に依存しているので、core-jsをいい感じにハンドリングしてコードが動く環境の設定に応じてpolyfillしてくれるやつ・・・という認識で良いと思います。

core-jsについて詳しく知りたければ次のリンクからどうぞ。

https://github.com/zloirock/core-js

#### ライブラリではないアプリケーションの開発者であれば、 `@babel/preset-env` を使って、 `entry` もしくは `usage` の設定でpolyfillする

`@babel/preset-env` というbabelプリセットを使ってpolyfillする方法です。

https://www.npmjs.com/package/@babel/preset-env

こちらも先のplugin同様、core-jsをいい感じにハンドリングしてpolyfillを行ってくれる機能を備えています。

webpackを利用している場合の、babel-loader込みの設定例は次の通りです。(`usage` の場合)
(`babel-loader`の他、`@babel/core`、`@babel/preset-env`、`core-js` がpackage.jsonに追加されている必要があります)

```webpack.config.js
const path = require("path");

module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist/usage"),
  },
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            targets: {
              ie: "11",
            },
            presets: [
              [
                "@babel/preset-env",
                {
                  useBuiltIns: "usage",
                  corejs: "3.21",
                },
              ],
            ],
          },
        },
      },
    ],
  },
};
```

`useBuiltIns: "usage"` となっていますが、これはアプリケーションの利用箇所に応じてpolyfillの注入をハンドリングしてくれる設定です。

webpack等を使い、polyfillの注入対象をアプリケーションコードと別のチャンクに分離するなど、polyfillによるコードサイズの最適化を図る場合には、`entry`を使ってまとめて分離すれば良いですが、とりあえずpolyfillできればいいや、という場合は、`usage` の設定で十分事足りると思います。

先述のいずれのpolyfillプラグインを使う場合でも、babelがpolyfillとは関係なく生成するヘルパー関数は各コードに分散したままになるので、それらのヘルパー関数をまとめたい場合には別途`@babel/plugin-transform-runtime` を使う必要がありますが、そこまで最適化を考えていない場合には先述のwebpack.config.jsの設定だけでも問題ないでしょう。
(@babel/plugin-transform-runtimeは、polyfillの文脈とヘルパー関数の集約の文脈の２つの文脈で出てくるので注意が必要です。今言及したのは後者の文脈です。)

### core-jsを直接使ってpolyfillを実現する

https://github.com/shoutatani/polyfill-playground/tree/main/polyfill-core-js-directly

次にご紹介するのは、core-jsを直接使ってpolyfillを実現する方法です。
tsc等、babelを通さない環境で使うこともあるでしょうか。

直接使う場合のコード例は以下の通り、core-jsをどこかのファイルでインポートするだけです。
これだけでglobalに副作用をもたらす形としてpolyfillが注入されます。

```polyfill-stable.js
import "core-js/stable";
```

もし、上記のように1ファイルにcore-jsの関心を分離させる場合には、その後webpack等のバンドラで結合させるか、もしくは独立したチャンクとして実行環境側で読み込めばいいかと思います。

webpackで結合させる場合のコード例も載せておきます。

```webpack.config.js
const path = require("path");

module.exports = {
  entry: [
    "./src/polyfill-stable.js",
    "./src/index.js"
  ],
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist/polyfill-core-js-stable"),
  }
};
```

この場合、stableなcore-jsの機能が副作用を起こして注入されるので、babelのようにtargetsに応じて・・・というものはありません。
いたってシンプルです。

したがって、末端のアプリケーションで利用する & JavaScriptコードのサイズを気にしないのであればこれでもいいかもしれません。
ライブラリ作者であれば、core-jsを直接バンドルして配布するのはサイズが大きくなるし副作用ももたらすので適していないのではないでしょうか。

なお、バンドラすら使いたくないという人のために、[CDNからも直接利用できるようです](https://cdn.jsdelivr.net/npm/core-js-bundle@3.24.1/minified.js)。
(CDN経由では使ったことないですが)

### swcを使ってpolyfillを実現する

https://github.com/shoutatani/polyfill-playground/tree/main/polyfill-swc

最後にご紹介するのは、swcを通してバンドルする際にpolyfillも行うやり方です。babelと似たようなものと思ってくれていいのではないでしょうか。

https://swc.rs/

webpackを利用している場合の、swc-loader込みの設定例は次の通りです。
(`swc-loader`の他、`@swc/core`、`core-js` がpackage.jsonに追加されている必要があります)

```polyfill.js
import "core-js/stable";
```

```webpack.config.js
const path = require("path");

module.exports = {
  entry: [
    "./src/polyfill.js",
    "./src/index.js"
  ],
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist/polyfill-swc-with-ie-11"),
  },
  module: {
    rules: [
      {
        test: /\.m?js$/,
        exclude: /(node_modules)/,
        use: {
          loader: "swc-loader",
          options: {
            "env": {
              "targets": {
                "ie": "11"
              },
              "mode": "entry",
              "coreJs": 3
            }
          }
        }
      }
    ]
  }
};
```

2022年8月現在、babelのpreset-env同様に、`usage` と `entry` の二通りが使えるようですが、
`The usage mode is currently not as efficient as Babel, yet.` の記載があるので、こちらは `entry` モードでのみ使用すべきでしょう。

`usage` と `entry` の使い分け・違いについてはbabelの欄をご覧ください。

babelよりも高速と謳われているので、babelと同様の機能性・安定性があると判断できたらこちらを使ってもいいのではないでしょうか。

## 他参考にすべきサイト

もっとpolyfillに関して詳しく知りたければ、
[ライブラリ作者におすすめしたいBabelの新機能 babel-plugin-polyfill-corejs3](https://www.wantedly.com/companies/wantedly/post_articles/408339) を一読してみてはどうでしょうか。すごく詳しく解説されています。(書いているのは僕ではないのでご注意を)

## おわりに

IEがサポート切れたからといって、JavaScriptのproposalな機能を使ったりする場合にはpolyfillのことを考えなければいけません。
今作っているアプリケーションはどのような実行環境であれば動かせることができるのか、そしてpolyfillは必要なのかどうか・・・アプリケーションを構築する必要は今までもこれからも気にしなければいけないでしょう。