🐕

@babel/plugin-transform-runtime を理解する(Babel 7)

2021/09/17に公開

概要

Babelの主要プラグインの一つである、@babel/plugin-transform-runtime に関して、雰囲気で使わずに理解を深めるためにまとめた内容です。

概ね上記公式ドキュメントに書いてある内容を要約しただけになります。

バージョン情報

以下で動作確認済み

  • Node v12.16.3
  • @babel/core v7.4.5
  • @babel/plugin-transform-runtime v7.4.5
  • @babel/runtime@ v7.9.2
  • core-js v3.x

@babel/plugin-transform-runtime is 何

本来はBabelによって注入されるヘルパーコードを、再利用可能なヘルパーをインポートする形に変換するプラグインです。これによってグローバル汚染を避けながら、成果物のコードサイズを小さくすることができます。

ES6+のインスタンスメソッドの使用にはcore-js@3が別途必要です。@babel/preset-envuseBuiltInsを設定するのが手っ取り早いですがここでは割愛します

インストール

@babel/plugin-transform-runtime はコードビルド時に必要なプラグインになるので、devDependencyで充分です。

$ yarn add -D @babel/plugin-transform-runtime

ビルド後のコードから参照されるヘルパー本体が別途必要になります。こちらは実行用なのでproductionDependencyになります。

$ yarn add @babel/runtime

設定

.babelrcなどの設定ファイル内に使用するプラグインを追加するだけ。オプションも豊富ですが、基本的にはデフォルトで問題無さそう。

{
  "plugins": ["@babel/plugin-transform-runtime"]
}

なぜ必要なのか

Babelは通常、一般的によく使われる関数(Class生成など)に対して、都度ヘルパー関数を生成しますが、アプリケーションのファイルが分散している場合、ヘルパーを必要とするそれぞれに対して同様のコードを複製してしまいます。

そうすると複製したコードが溜まっていき、全体のファイルサイズが大きくなってしまい、アプリケーションの初期読み込み時間が長くなる問題に繋がってしまいます。

そこで @babel/plugin-transform-runtime を用いると、ヘルパーを必要とするそれぞれのファイルがヘルパー関数を定義するのでなく、@babel/runtime を参照するように変更されます。必要な関数は全て @babel/runtime に配置してあるので、コードがどれだけ分散していても全体のファイルサイズに影響を与えません。

また、@babel/plugin-transform-runtime を使わずに、babel/polyfillなどを直接インポートして変換すると、グローバル汚染が発生し、そのコードを外部ライブラリとした他のコードに影響を与える可能性があります。 (逆に言えばアプリケーションやコマンドラインツールであれば大した問題にはならない)

CLIで挙動を見る

実際の挙動をCLIで確認するとイメージが湧くので、まずbabel-cliと、preset-envcore-jsも用意しておきます。

$ yarn add @babel/core
$ yarn add -D @babel/cli @babel/preset-env core-js@3

babelの設定は必要最低限に。とりあえず IE11 をサポートするようにしておけばだいたいのコードは polyfill されます。

{
  "presets": [
    ["@babel/preset-env", {
      "modules": false,
      "targets": {
        "browsers": "> 1%",
	"ie": 11
      },
      "corejs": {
        "version": 3,
        "proposals": false
      },
      "useBuiltIns": "usage"
    }]
  ],
  "plugins": [
  ]
}

検証対象のスクリプトは以下のような、クラスを通じて文字列を出力する程度のものになります。

class Human {
  constructor(name) {
    this.name = name
  }
  sayHello() {
    return `Hello, ${this.name}`
  }
}

const human = new Human('sasaki')
console.log(human.sayHello())

babel-cliを実行すると、es5に変換されます。

$ yarn babel script.js
import "core-js/modules/es.function.name";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

var Human = /*#__PURE__*/function () {
  function Human(name) {
    _classCallCheck(this, Human);

    this.name = name;
  }

  _createClass(Human, [{
    key: "sayHello",
    value: function sayHello() {
      return "Hello, ".concat(this.name);
    }
  }]);

  return Human;
}();

var human = new Human('sasaki');
console.log(human.sayHello());

特筆すべきは、 _classCallCheck _defineProperties _createClass と言った、ES5でES6+相当と同等の仕組みを提供するためにbabel(正確にはbabelプラグイン)が生成したヘルパー関数です。

では次に、 @babel/plugin-transform-runtime を有効にしてみます。

$ yarn babel --plugins @babel/plugin-transform-runtime script.js
import "core-js/modules/es.function.name";
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _createClass from "@babel/runtime/helpers/createClass";

var Human = /*#__PURE__*/function () {
  function Human(name) {
    _classCallCheck(this, Human);

    this.name = name;
  }

  _createClass(Human, [{
    key: "sayHello",
    value: function sayHello() {
      return "Hello, ".concat(this.name);
    }
  }]);

  return Human;
}();

var human = new Human('sasaki');
console.log(human.sayHello());

おや? 先程まで直接生成されていたヘルパーコードが、

import _classCallCheck from "@babel/runtime/helpers/classCallCheck";`

のように、他のモジュールを参照するように振る舞いが変わっていますね。

ではこの @babel/runtime/helpers/classCallCheck を見てみましょう。

function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError("Cannot call a class as a function");
  }
}

module.exports = _classCallCheck;

本来は動的に生成されていたコードがこちらに配置されてることが確認できました。

これが @babel/plugin-transform-runtime のほかに @babel/runtime を、それもProductionに入れる必要があるのはこういうワケでした。

GitHubで編集を提案

Discussion