CLOVER🍀

That was when it all began.

Jest+TypeScriptを高速に実行したい(ts-jest、esbuild、SWCを比べる)

これは、なにをしたくて書いたもの?

最近、TypeScriptを使っているわけですが、テストコードを書くとJestを使います。

Jestのドキュメントに習うとTypeScript+Jestではts-jestを使ってテストを実行することになるのですが、これが遅いのでどうにかならないかな?
ということで調べてみました。

ソースコード本体の方はtsc --watchでいいかなと思っているのですが、テストコードの方はnpm testで実行したいなとも思うので…。

Jest+TypeScript

Jestのドキュメントに習うと、TypeScriptでJestを使う場合はBabelまたはts-jestを使うことになります。

Getting Started / Using TypeScript

自分は、ts-jestを使っています。

ts-jest以外の選択肢

esbuildとSWCがあるようです。どちらも、TypeScriptのサポートとJestに対するtransformerがあります。

esbuild。

esbuild - An extremely fast bundler for the web

GitHub - aelbore/esbuild-jest: A Jest transformer using esbuild

SWC。

Rust-based platform for the Web – SWC

@swc/jest – SWC

どちらもまったく知らないので、軽くドキュメントなどを眺めてみます。

esbuild

esbuildは、高速なJavaScriptのバンドラーであると謳っています。Goで実装されているようです。

esbuild - An extremely fast bundler for the web

GitHub - evanw/esbuild: An extremely fast bundler for the web

今のWeb用のビルドツールは10〜100倍遅くなっているため、高いパフォーマンスで、使いやすいバンドラーを作ることがesbuildの目標だと
されています。

Our current build tools for the web are 10-100x slower than they could be. The main goal of the esbuild bundler project is to bring about a new era of build tool performance, and create an easy-to-use modern bundler along the way.

主な機能。

  • 高速
  • ES6およびCommonJSのサポート
  • ES6モジュールのTree shaking
  • JavaScriptおよびGo向けのAPI提供
  • TypeScript、JSXのサポート
  • Source Map
  • Minify
  • プラグイン

今回の主題である、TypeScriptに関するドキュメントはこちら。

Content Types / TypeScript

TypeScriptのサポートはデフォルトで有効になっています。

注意点としてはesbuildは型チェックを行わないので、tsc --noEmitを合わせて使う必要があります。

However, esbuild does not do any type checking so you will still need to run tsc -noEmit in parallel with esbuild to check types.

その他、注意点。

Content Types / TypeScript caveats

  • ファイルは独立してコンパイルされる
  • importは、ECMAScriptモジュールとして振る舞う
  • 型システムに関する機能はサポートしていない
  • tsconfig.jsonの特定の項目のみを考慮する
  • *.tsファイルにtsxローダーを使用できない

Jest向けのTransformerは、こちらがあります。

GitHub - aelbore/esbuild-jest: A Jest transformer using esbuild

SWC

SWCは、Rustで実装された次世代の開発者向けツールとされています。Next.js、Parcel、Denoなどで利用されているのだとか。

Rust-based platform for the Web – SWC

GitHub - swc-project/swc: Rust-based platform for the Web

コンパイルおよびバンドルの両方に使用でき、JavaScriptとTypeScriptをサポートしています。

設定は.swcrcというファイルで行い、

Configuring SWC – SWC

この中にTypeScriptに関する設定項目があります。

Compilation / jsc.parser / typescript

ただ、SWCもesbuildと同様、TypeScriptの型チェックは行わないようです。

TypeScript type checker · Issue #571 · swc-project/swc · GitHub

TypeScript support for type-checking · Issue #126 · swc-project/swc · GitHub

Swc can also compile typescript / tsx to ecmascript. Note that it does not type-check at the time of writing.

Introducing SWC 1.0 / TypeScript support

Jestについてはサポートがあり、Transformerが提供されています。こちらをts-jestの代わりに利用できます。

@swc/jest – SWC

説明は、これくらいにして使っていってみましょう。

環境

今回の環境は、こちら。

$ node --version
v16.13.1


$ npm --version
8.1.2

お題

簡単なテストプログラムを、以下のバリエーションで書いて確認したいと思います。

  • JavaScriptのみ
  • TypeScript+ts-jest
  • TypeScript+esbuild
  • TypeScript+SWC

以下のディレクトリを作り、それぞれ作っていきます。

$ mkdir jest-js jest-ts-jest jest-ts-esbuild jest-ts-swc

JavaScriptのみ

まずは、JavaScriptのみ。

$ cd jest-js

ソースコード、テストコード用のディレクトリ作成。

$ mkdir src test

npmプロジェクト作成。

$ npm init -y

Prettierのインストール。

$ npm i -D -E prettier

設定。

.prettierrc.json

{
  "singleQuote": true
}

Jestのインストール。

$ npm i -D jest

jest --initはしましたが

$ npx jest --init

結局、内容はこれくらいになりました。

jest.config.js

module.exports = {
  testEnvironment: "node",
};

依存関係。

  "devDependencies": {
    "jest": "^27.4.7",
    "prettier": "2.5.1"
  }

ソースコード

src/calc.js

function plus(a, b) {
  return a + b;
}

function minus(a, b) {
  return a - b;
}

module.exports = { plus, minus };

テストコード。

test/calc.test.js

const calc = require('../src/calc');

test('plus', () => {
  expect(calc.plus(1, 2)).toBe(3);
});

test('minus', () => {
  expect(calc.minus(5, 1)).toBe(4);
});

実行時間は、これくらいです。
※なんとなく、jestコマンドで実行することにしました

$ npx jest
 PASS  test/calc.test.js
  ✓ plus (2 ms)
  ✓ minus (1 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.239 s, estimated 1 s
Ran all test suites.

ここから、他のケースを見ていきます。

TypeScript+ts-jest

次は、TypeScriptとts-jestで。

$ cd jest-ts-jest

先ほどのnpmプロジェクトのセットアップに、TypeScriptを追加。

$ mkdir src test
$ npm init -y
$ npm i -D typescript
$ npm i -D -E prettier
$ npm i -D @types/node@v16

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "baseUrl": "./src",
    "outDir": "dist",
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noPropertyAccessFromIndexSignature": true,
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

.prettierrc.jsonは、先ほどと同じです。

Jestおよびts-jestのインストール。

$ npm i -D jest @types/jest ts-jest

設定ファイルの作成。

$ npx ts-jest config:init

jest.config.js

/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
};

依存関係。

  "devDependencies": {
    "@types/jest": "^27.4.0",
    "@types/node": "^16.11.19",
    "jest": "^27.4.7",
    "prettier": "2.5.1",
    "ts-jest": "^27.1.2",
    "typescript": "^4.5.4"
  }

ソースコード

src/calc.ts

export function plus(a: number, b: number): number {
  return a + b;
}

export function minus(a: number, b: number): number {
  return a - b;
}

テストコード。

test/calc.test.ts

import { plus, minus } from '../src/calc';

test('plus', () => {
  expect(plus(1, 2)).toBe(3);
});

test('minus', () => {
  expect(minus(5, 1)).toBe(4);
});

実行。

$ npx jest
 PASS  test/calc.test.ts
  ✓ plus (2 ms)
  ✓ minus (1 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.761 s, estimated 2 s
Ran all test suites.

JavaScriptの時より、7倍くらい時間がかかります。

これをなんとかしたい、と。

TypeScript+esbuild

次は、先ほどのプロジェクトをts-jestからesbuildに変更したものを作ります。

$ cd jest-ts-esbuild

セットアップ。

$ mkdir src test
$ npm init -y
$ npm i -D typescript
$ npm i -D -E prettier
$ npm i -D @types/node@v16

tsconfig.jsonおよび.prettierrc.jsonは、先ほどと同じなので省略。

Jestのインストールの際は、ts-jestを入れないようにします。

$ npm i -D jest @types/jest

esbuildおよびesbuild-jestのインストール。

$ npm i -D esbuild-jest esbuild

Jestの設定。transformeresbuild-jestを指定します。

jest.config.js

module.exports = {
  testEnvironment: 'node',
  transform: {
    "^.+\\.tsx?$": "esbuild-jest"
  }
};

ちなみにJestの設定をTypeScriptでも書けますが、結局遅くなるのでJavaScriptにしています。

依存関係。

  "devDependencies": {
    "@types/jest": "^27.4.0",
    "@types/node": "^16.11.19",
    "esbuild": "^0.14.11",
    "esbuild-jest": "^0.5.0",
    "jest": "^27.4.7",
    "prettier": "2.5.1",
    "typescript": "^4.5.4"
  }

ソースコードおよびテストコードは、ts-jestの時と同じです。

テストを実行。

$ npx jest
 PASS  test/calc.test.ts
  ✓ plus (2 ms)
  ✓ minus

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.215 s, estimated 1 s
Ran all test suites.

速いです。めちゃくちゃ速いです。JavaScriptのみの時と同等か、それ以上に速いくらい。

すごいですねぇ…。

型チェックが外れているんですが。

TypeScript+SWC

最後に、SWCです。

$ cd jest-ts-swc

プロジェクトのセットアップ。

$ mkdir src test
$ npm init -y
$ npm i -D typescript
$ npm i -D -E prettier
$ npm i -D @types/node@v16

tsconfig.jsonおよび.prettierrc.jsonは、ここまでと同じ。

Jestのインストール。

$ npm i -D jest @types/jest

SWCのJestモジュールのインストール。

$ npm i -D @swc/jest

Jestの設定。transformer@swc/jestを指定。

jest.config.js

module.exports = {
  testEnvironment: 'node',
  transform: {
    "^.+\\.tsx?$": "@swc/jest",
  }
};

ソースコードおよびテストコードも、ここまでと同じ。

テストを実行。

$ npx jest
 PASS  test/calc.test.ts
  ✓ plus (2 ms)
  ✓ minus

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.217 s, estimated 1 s
Ran all test suites.

こちらも速いです。esbuildと同等ですね。

まとめ

TypeScriptを使ったJestのテスト実行を速くしたいなと思い、esbuildおよびSWCを調べつつ、素のJavaScriptとts-jestとの比較をやってみました。

esbuild、SWC、ともに速いですね。

型チェックをスキップしているのでそこは難点なのですが、それは別途tscでカバーできるように考えてみようかなと思います。
テストコードとはいえ、完全に型チェックを外すと意味ないので…。