🕌

Native ESM 時代のフロントエンドビルドツールの動向

2021/03/09に公開

No Bundle ツールの流行: vite / snowpack

モダンブラウザは Native ESM を備えているので、開発時は高速な localhost アクセスを頼って直接 import する、外部ライブラリだけ事前にコンパイルしておく、という手法が流行ってきている。プロダクション用は今まで通りビルドする。

webpack はすべてを一つにバンドルするためにメモリ上にファイルの実体と依存グラフを持っているが、これによりメモリと CPU を圧迫する問題があった。特に巨大なリポジトリではそれが顕著になる。

No bundle ツールの実装として vite と snowpack がある。

vite は使ってみた限り、更新時の差分ビルドが爆速で、明らかに体感が良い。 Vue の Evan You がこれにリソースを集約するのもうなずける。 内部的には vite v2 から rollup のインターフェースがむき出しになってて、 rollup のプラグインなら何でも使える。ここにエコシステムの広がりと、将来の混乱の発生源になるであろう闇を感じた。

Evan You が作ってるとはいえ Vue のためのものというわけではなく、 Rollup / Svelte 作者の Rich Harris も Sveltekit (Next.js 風 Svelte フレームワーク) を vite で実装すると Twitter で話していた。

https://twitter.com/Rich_Harris/status/1367577006355976194

snowpack は自分で試す限りは品質面に問題があって、巨大なファイルを食わせるとビルドが不安定になり、プロセスが port を開放してくれなかったりで、結構困った。また、独自のプラグイン形式の自由度が狭く、詰むことがある。自分もバグを一つ報告して直してもらったが、設計面の問題は治りそうにないので、 vite に移行した。snowpack が完全にだめだと言うわけではなく、昔の next.js も思想に実装が追いついてなくて似たような状態だったので、今後に期待。

というわけで、しばらく小規模なものや、ライブラリの試し切り的なプロジェクトでは vite を使ってみようと思っている。

ESM CDN

という通称があるわけではないが勝手に命名した。

これも主に開発時に native ESM を使うもので、 npm をバックエンドにして commonjs で書かれたものを esm に変換して配布する CDN がいくつか出てきている

  • skypack.dev: snowpack の開発者がホストしている、ESM に prebuild された JS を配る CDN
  • esm.sh: esbuild を使った skypack と似たコンセプトの CDN

snowpack がオプションを有効にすると skypack を使う。開発時のプレビューや、ESM しかサポートしない deno が esm.sh や skypack をバックエンドに使うことがある。

どちらもアクセス時に cloudflare workers を挟んでいて、 cache がなければ edge 上でビルドし、 KV Workers の edge cache に保存する(のでビルドされていないファイルを呼ぶと初回アクセスが遅い)。こういうところに cloudflare workers の使い道があるのが面白い。

Rollup

そんな中で存在感が増してるのが rollup で、今まではライブラリ作者が ESM を出力したいときに使うものだったが、 No bundle ツールだと一級市民になる。

rollup.js

snowpack も vite も内部的に rollup を使っている。 Rollup は Webpack とは違って、 ESM First な設計で、commonjs は標準ではサポートされていない。 commonjs を esm に変換する、webpack 時代と逆の発想のプラグインを使う必要がある。

で、使ったことがある人はわかると思うが、 @rollup/plugin-node-resolve@rollup/plugin-commonjs がかなり複雑怪奇な動きをする。 vite や snowpack の中では比較的こなれた plugin 定義が挟まっているが、たぶん無限にエッジケースがある。 node_modules の呼び出しでハマりたくなければ、ライブラリ配布者は rollup で format: "es" で ESM 用にビルドしたものを配布しておくとよいし、ライブラリを利用者も package.json に module: 'dist/index.es.js' のような module が指定されているものを選んでおくと将来的に困らない(し、そういう実装がなされているのはアクティブにメンテナンスされているライブラリなのでたいてい困らない)

結局 webpack から vite に移行したくなっても、 esm でビルドされた依存がどれだけあるか次第になる気がしていて、普段から行儀の悪いライブラリをどれだけ避けているか? という徳次第になりそう。

Transpiler のトレンド

最近は TS 一強で、その際の TS コンパイルのボトルネックが明らかになっていて、とにかく TypeScript をいかに速くコンパイルできるか、という次元での勝負になっている。

esbuild は Go で書かれているから速いというより、構文解析すっ飛ばして一回のパスでコンパイルするから速い。同じアプローチで JS で実装してもたぶんそこそこ速くなる。

標準の typescript コンパイラの役割がなくなったわけではなく、 language service として解析を行うのはこっち。あくまで型情報を高速に落とす、という目的で esbuild と swc がある。

Node ESM と Deno

Node v14 から Native ESM をサポートして、相互に呼べるようになった。しかし、 babel と typescript の ESM 用の interop (exports.__esModules = true みたいなやつ)が逆に邪魔になっており、ここでも結局 rollup を使ってビルドする必要がある。

Node.js Native ESM への道 〜最終章: Babel / TypeScript Modules との闘い〜

Deno は最初から ESM しかサポートしていない。逆に言うと、 node の native module の polyfill まで入れてビルドしたものは Node と Deno 両方で動く。最近はそういうライブラリをちらほら見かけるようになってきた。

nanojsx/nano: 🎯 SSR first, lightweight 1kB JSX library.

skypack と esm.sh は両方とも deno 向けに x-typescript-types というヘッダを返していて、ここで指定された URL で型定義ファイルを返す、という実装になっている。 ESM CDN 系のものは、モジュール解決で直接 URL を指定する Deno を最初から考慮している。

Deno が結構安定してきており、そろそろフロントエンドのツールチェイン周りでは使っていけるのではないか、という気がしている。サーバーは怖い。とりあえず Deno と Node 両対応するのがライブラリ作者のトレンドになるのではないか。

最後に

特殊な webpack-loader は避けて、 webpack は最小限に使って Native ESM に備えておけ、と昔から言っていたのだが、やっぱりそういう時代になりつつある。ただし一旦は rollup plugin に変わるだけだが。

俺の webpack.config.js-20200503 - mizchi's blog

開発者環境はそうだけど、じゃあ native esm をプロダクションで使わせろという話をちらほら聞く。が、たぶんあと 10 年は使えない

IE は無視したとしても、 RTT の問題で現実的にはならない。server side push が死んだ今、モジュールのサーバーサイドからの先読みで代替となる仕様がない。もしかしたら ESM CDN のように edge でビルドする時代になる時代になるのかもしれない。

<link rel="modulepreload" href="/index.mjs"> のような module preload を行うことができるが、対処療法であって、根本的な解法にはならない。

最終出力する階層が浅くはなるが、結局ビルドし続けることになると思う。

Preloading modules  |  Web  |  Google Developers

Preload を用いたリソースプリローディングの最適化 | blog.jxck.io

Discussion