依存パッケージのバンドルサイズを正確に計測したい。アプリケーションがある程度肥大化したあとでのチューニングより技術選定時点の参考情報にするため。実際にアプリケーションを作り始めたあとでは乗り換えることが困難であるため、正確な計測値がわかるなら事前に知っておきたいという需要に向けて。swr と@tanstack/react-query どっち使うかとか、recoil と jotai どっち使うかとか。
- Web
- Middleware
- CLI
- VS Code Extension
有名所が Bundlephobia だが、これらの解析ツールのほとんどはパッケージのインストールサイズをターゲットにしている。詳しくはpackage.json
でfiles
に指定したものがインストールされるすべて。すなわち、未使用のモジュール、型定義ファイル、package.json
、 README.md
などを含んだ合計値を算出してしまう。たとえば、利用者への利便性に配慮しsrc
を同梱した場合にもそのサイズ分膨れ上がってしまう。Dual Package 対応のライブラリでは ESM 用と CJS 用で単純に 2 倍になる。
ビルド後のバンドルに含まれる Tree Shaking 後のパッケージをより正確に知りたいならミドルウェア系の Bundle Analyzer を使うのが確実だ。結局、実際にアプリケーションをバンドルしないとわからない。ただ我々は ESM の Tree Shaking を考慮した情報がほしいところ、mizchi さん(@mizchi)が同じ問題意識で作成した計測サイト(Shakerphobia)がある1。簡易にでも計測したいという需要は満たせそうだ。
Shakerphobia を使って実践的に計測していこう。まず、Bundlphobia と pkg-size の計測結果を見てみよう。
圧倒的に Recoil のバンドルサイズが大きいことがわかるだろう。続いて Tree Shaking 込みのバンドルサイズを見てみよう。
Recoil のインポートする API を増やして再計測してみる。
import { atom, useRecoilState, useRecoilValue, useSetRecoilState } from "recoil";
Tree Shaking 後のバンドルサイズが微量しか増えていない。理由に挙げられそうなのは、內部の共通コアロジックが大きいか、ビルド形式が良くないか。後者を考えると、Recoil は単一のファイルにバンドルしている。このため、利用側のバンドラーによる解析が難しく Tree Shaking が効いてないのではないかと推測できる。バンドラーはモジュール単位の依存関係を解析し、DCE(dead code elimination)の参考情報にする。単一のファイルではそれ自体がエントリーポイントになるため、export しているものがすべて DCE の対象外になる可能性がある。(要検証)
Recoil/rollup.config.js at main · facebookexperimental/Recoil
// ES
{
input: inputFile,
output: {
file: `es/recoil.js`,
format: 'es',
exports: 'named',
},
external: externalLibs,
plugins: commonPlugins,
},
Recoil/package.json at main · facebookexperimental/Recoil
{
"name": "recoil",
"main": "cjs/index.js",
"module": "es/index.js",
"react-native": "native/index.js",
"unpkg": "umd/index.js",
"files": ["umd", "es", "cjs", "native", "index.d.ts"]
}
Jotai の方も載せておく。最終的にはsrc/utils.ts
がdist/esm/utils.mjs
に出力される。これはモジュール構造を維持したままビルドされることを表す。なので、Jotai はjotai/utils
のように、基本的な API 以外の API はプラガブルな形でインポートする。逆に、Recoil はオールインワン形式だ。
jotai/package.json at main · pmndrs/jotai
{
"name": "jotai",
"version": "2.2.2",
"exports": {
"./package.json": "./package.json",
".": {
"types": "./index.d.ts",
"import": {
"types": "./esm/index.d.mts",
"default": "./esm/index.mjs"
},
"default": "./index.js"
},
"./*": {
"types": "./*.d.ts",
"import": {
"types": "./esm/*.d.mts",
"default": "./esm/*.mjs"
},
"default": "./*.js"
}
},
"files": ["**"]
}
以上からわかるように、バンドルサイズが厳しい環境をターゲットにしているとか、ただReact.Context
を代替するような使い方がしたい場合なら、Jotai がシンプルに始められるので適しているといえる。Recoil の方の利点というと、複雑なことができるように API が豊富だ2。結論として、複雑なアプリケーションには Recoil を、小さなアプリケーションには Jotai を選ぶのが良さそうだ。
- 単一ファイルにバンドルしない
- デフォルトエクスポートより名前付きエクスポート
単一ファイルにバンドルした場合(index.js にすべての公開するファイルをまとめる)、Tree Shaking が効かないので避ける。最小構成で使うときの単位にデフォルトエクスポートを使うのはいいと思うが、バンドラーの気持ちになると解析しやすい名前付きエクスポートが好ましい。