tldr

  • target: es2017 以降なら tslib 使っても置き換えられるコードはないので、使う必要はない
  • target: es5/es2015 で async await を多く使っている場合はファイル数に比例して削れる

importHelpers / tslib とは

typescript で async/await をコンパイルすると次のようなコードを生成する

var __awaiter =
  (this && this.__awaiter) ||
  function (thisArg, _arguments, P, generator) {
    function adopt(value) {
      return value instanceof P
        ? value
        : new P(function (resolve) {
            resolve(value);
          });
    }
    return new (P || (P = Promise))(function (resolve, reject) {
      function fulfilled(value) {
        try {
          step(generator.next(value));
        } catch (e) {
          reject(e);
        }
      }
      function rejected(value) {
        try {
          step(generator["throw"](value));
        } catch (e) {
          reject(e);
        }
      }
      function step(result) {
        result.done
          ? resolve(result.value)
          : adopt(result.value).then(fulfilled, rejected);
      }
      step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
  };

これがファイルごとに生成されるのでビルドサイズが増える。これは async/await に限らず、 object rest spread, generator などもこのようなヘルパとともに変形される。
tslib はこれらのコードをライブラリとして提供して、 importHelpers を有効にすると、tslib から __awaiter などを参照するようになる。

これにより、ファイル数が多いプロジェクトではビルドサイズを削減することが可能。

有効になる条件

結構ややこしかった。

  • tsconfig.json: "importHelpers": true
  • tsconfig.json: "moduleResolution": "node" がないとコンパイルはでないが警告が出る
  • ビルドターゲットのどこかで tslib を import/require する

2 番目と 3 番目が分かりづらいが、試した限りそういう挙動をした。

実験 / セットアップ

tsconfig.json

{
  "include": ["src"],
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "lib",
    "noEmitHelpers": true,
    "importHelpers": true,
    "target": "es2019",
    "module": "esnext",
    "lib": ["DOM", "ES5", "ES2015"],
    "strict": true,
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

これをベースとする。

rollup.config.js

import { nodeResolve } from "@rollup/plugin-node-resolve";
export default {
  input: "lib/index.js",
  plugins: [nodeResolve()],
  output: {
    format: "iife",
    file: "dist/bundle.js",
  },
};

中間状態をみたいので、 yarn tsc -p . && rollup -c rollup.config.js のようなコマンドで、一旦 tsc コマンドで lib に吐いてからそれを rollup でバンドルした。

code

こういうコードを用意した。

// src/index.ts
import "tslib";

import { sub } from "./sub";

const obj = { ...{ a: 1 }, ...{ b: 2 } };
async function main() {
  await sub();
  console.log("xxx", obj);
}

main();
// sub.ts
export async function sub() {
  new (class {})();
  return await Promise.resolve({ ...{ a: 1 }, ...{ b: 2 } });
}

あえて async/await や rest spread を使った。rollup に treeshake されないように気をつけている。

ビルド結果

target/importHelpers を変えながらビルドした結果

  • es2019/true: 289B
  • es2019/false: 289B
  • es5/true: 4.7K
  • es5/false: 6.9K

es2019 には効果がない。
es5/es2015 で async/await を使うとそれなりに効果が出る。

おわり

個人的に、モダンブラウザをターゲットにする場合は es2017/es2019 を指定するので、importHelpers を使う必要はないと感じた。
IE をターゲットに含む場合で、かつ何が何でも 1kb でも削りたい、と、ビルドサイズを突き詰めたいときに便利かもしれない。しかし、IE のために涙ぐましい努力をするより、nomodule などを使ってビルドを分けてしまう方がパフォチュしやすく、またシェア 5%の IE のために努力するのも、今では効果が薄い。

Browser Market Share Japan | StatCounter Global Stats

今後さらに pipeline operator のようなコードが出てきたときに、また有用なヘルパが出てきたときに考えると良さそう。

History
386c2c5 - fix Sat Aug 8 17:37:35 2020 +0900