TL;DR
- ちょうど1ヶ月前に
Typiaに commit をはじめてからというもの、Bundle Sizeの削減に取り組んできました - 大幅に Tree-shaking が改善し、Bundle Size が削減されました (65.99 KB -> 2.53 KB!)
- Bundle Size が気になる Frontend や Edge Worker にも安心して使えるようになりました
- 今後も
Typiaへの commit を続けていきます。
https://github.com/ryoppippi/thesis-benchmarks https://typia.io
はじめに
約1ヶ月前にこんな記事を書きました。
/blog/2024-06-12-zenn-c4775a3a5f3c11-ja
Typia については上の記事を読んでいただけると嬉しいのです。
簡単に Typia について説明すると、Typia は TypeScript 向けのValidation Library です。
ただし、既存の Library とは違い、Typia は TypeScript の型システムからValidation Logic を生成するという特徴があります。
- Library ごとの独自の記法を用いることなく、TypeScript の型システムをそのままValidationに使用できる
- ビルド時に Validation Logic を生成するため、非常に高速である
という特徴があります。
Bundle Sizeが大きすぎる問題
さて、上にあげた記事の最後の部分で、Typia の Bundle Size が大きい問題があると書きました。
実際、Valibotの作者である Fabian Hiller 氏の論文 によると、Typia は高速ではあるものの、Bundle Size が大きいという問題がありました。
https://valibot.dev/thesis.pdf
実際に issue にもなっていました。
https://github.com/samchon/typia/issues/752
この Bundle Size 問題について、ここ1ヶ月ほどかけて改善をしていきました。
この記事では、Typia の Bundle Size を削減するために行った手法を紹介します。
振り返ってみると、よく知られた手法だったり、当たり前のことだったりしますが、自分にとっては新たな学びが多かったので、宣伝を兼ねて記事にしました。
改善結果
と、その前に、先に結果を書いてしまいます。
比較にはValibotの作者である Fabian Hiller 氏の論文が行った実装をベースに以下のような手を加えたものを使用しました。
- 論文に使われた Schema に加え、よりシンプルな Schema を追加
- Bundle Size の計測には
rollupを使用。terserで Minify をした - 現実に則し、無圧縮の Bundle Size と
gzip圧縮後の Bundle Size を計測
実際のコードは以下のリポジトリにあります。 https://github.com/ryoppippi/thesis-benchmarks
自分がビルド環境に commit しはじめてからのバージョンごとの Bundle Size は以下のようになりました…
| Typia Version | Simpler Schema | Simpler Schema (Gzip) | Large Schema | Large Schema (Gzip) | Notes |
|---|---|---|---|---|---|
| 6.0.5 | 65.99 KiB | 14.51 KiB | 74.26 KiB | 15.43 KiB | Only CJS |
| 6.0.6 | 36.47 KiB | 10.1 KiB | 44.75 KiB | 11.03 KiB | First ESM Support |
| 6.4.0 | 6.76 KiB | 2.69 KiB | 15.04 KiB | 3.64 KiB | ESM with file splitting |
| 6.4.1 | 2.53 KiB | 1.1 KiB | 10.8 KiB | 2.06 KiB | Enable sideEffects=false |
| valibot(v0.35.0) | 4.01 KiB | 1.43 KiB | 6.05 KiB | 1.89 KiB | 参考 |
うおお! Typia の Bundle Size が大幅に削減されました!!!
さらに、Schema によっては valibot よりも小さくなっていることもわかります。
Note
Typia は Validation Logic を inline 展開します。そのため、Schema が大きいほど、同じ Logic が使用されている変数が Schema の内部で複数回出現するため、Bundle Size が大きくなります。
それに対して、Valibot は同じ Logic ならば関数を共有するため、Schema が大きくなっても Bundle Size が大きくなりにくいです。
Schema が小さい場合は、inline 展開する方が結果的に Bundle Size は小さくなります。
そのため、Typia は Schema が小さい場合には Valibot よりも小さくなることがあります。
パフォーマンスについては今回は手元では計測していませんが、元の論文によると、Typia は Valibot よりも高速であるとのことです。
では、それぞれの version で、どのような変更を行ったのか見ていきましょう。
6.0.6
6.0.5 以前は、Typiaは CommonJS (以下 CJS) のみを dist として提供していました。
しかし、6.0.6 からは ESM も提供するようになりました。
https://github.com/samchon/typia/pull/1067
そもそも、 CJS では tree-shaking が効きづらいという問題があり、ESM としての提供は必要だと考えました。
このバージョンからは rollup で ESM 形式の compile を行うようになりました。
6.1.0
サイズ変更には関係のない余談
このバージョンから ドキュメントでは unplugin-typia が setup の公式方法として紹介されるようになりました。
https://github.com/samchon/typia/releases/tag/v6.1.0 https://typia.io/docs/setup/#unplugin-typia
また、一部 Typia が依存している CJS 形式で配布されている Library の取り扱いについても対応が行われました。 Random generator を使う際にはこの最適化が効果的です。
6.4.0
6.4.0 では、ESM において、TypeScript のファイルの構造を保ったままで mjs ファイルを生成するよう rollup の設定を変更しました。
それ以前のrollupの設定では、ビルド時に全ての TypeScript ファイルを一つの index.mjs にまとめていました。
これでも問題ないだろうと考えていましたが、実際にはファイル分割を行うことで、Tree-shaking が効果的になることがわかりました。
なぜこのような Bundle Size の差が出たのかというと、Typia 内部で namespace import が多用されていたためです。
元々、Typia の内部では namespace が多用されていました。
namespace とは以下のような構文のことです。
namespace A {
export const a = 1;
export const b = 2;
}
これは一見便利そうに見えますが、namespace は JavaScript の構文ではないため、compile の結果には余計なコードが含まれてしまい Bundle Size が大きくなる原因になります。 これに関して、namespace を使わず、namespace import を使うよう過去の PR では対応されていました。
https://github.com/samchon/typia/pull/928
namespace import とは以下のような構文のことです。
import * as A from './A';
import * as B from './B';
export { A.foo, B.kuu };
この方法ならば、ビルド時に Bundler が元のファイルを探してきて、tree-shaking が有効になります。 一見効果的なように見えます。
しかし、提供する mjs ファイルを一つにまとめてしまうと、tree-shaking が十分に効かないことがわかりました。
これは以下の issue で議論されています。
https://github.com/evanw/esbuild/issues/1420
Note
簡単に上の issue をまとめると、 一つのファイルに全てを bundle してしまうと、namespace import で import された名前空間が一つのobject に変換されてしまい、tree-shaking が効かないというものです。
例えば
index.ts
import * as A from './A';
console.log(A.a);
A.ts
export const a = 1;
export const b = 2;
のようなコードがあったと仮定します。
理想的には、これらをビルドした時、A.b は使われていないので、A.b が含まれないようにしたいです。
index.mjs
import {'\{'} a {'\}'} from './A';
console.log(a);
A.mjs
export const a = 1;
しかし、これが Library としての compile 時に一つのファイルにまとめられてしまうと、A という namespace が一つの object にまとめられてしまいます。
index.mjs
const A = {'\{'} a: 1, b: 2 {'\}'};
console.log(A.a);
このように、実際は A.b は使われていないのにも関わらず、index.mjs には A.b が含まれてしまいます。
このようにして tree-shaking が悪化します。
これを防ぐためには、できる限り元の TypeScript の実装のファイル構造を保ったままで Library を提供し、 ユーザーによる最終成果物のビルド時に bundler が考慮できるようにする必要があります。
この問題を解決するために、6.4.0 では rollup の設定を変更し、元の TypeScript ファイルの構造を保ったままで mjs ファイルを生成し、これを提供するようにしました。
具体的には preserveModules: true を有効にして対応しました。
また、そのほかにもいくつかの設定を見直しました。
https://github.com/samchon/typia/pull/1133
(↑めっちゃ必死に必要性を訴えているのがわかる)
6.4.1
6.4.1 では、package.json の sideEffects を false に設定しました。
https://github.com/samchon/typia/pull/1146
sideEffects は以下のような設定です。
{
"sideEffects": false
}
これは、このパッケージが副作用を持たないことを示すものです。
sideEffectsに関しては、Typia のコード内で /*#__PURE__*/ というコメントが至る所で使われているため、設定は不要だと考えていました。
しかし、実際に最小環境を作成して検証をしてみると、sideEffects を設定していない時は、依存関係の一つである ret.js という Library が常に含まれてしまうことがわかりました。 ret.js は Typia の random generator の時にのみ使用される Library であり、それ以外の場面では使用されません。
明示的に sideEffects を設定すると、ret.js が含まれなくなり、Bundle Size がさらに削減されました。
この設定については以下の記事が参考になりました。 https://zenn.dev/uttk/articles/re-export-tree-shaking
Note
ちなみにこのsideEffectの議論を Fabian Hiller 氏と以下の issue で行っていました。
感謝いたします。
https://github.com/samchon/typia/issues/752#issuecomment-2209356169
7.0.0 …?
おそらく内部の実装に手を入れずに Bundle Size をこれ以上削減するのは難しいと考えています。
そのため、7.0.0 では内部のコードのリファクタリングを行い、さらに Bundle Size を削減する予定です。
楽しみですね!
まとめ
Typia の Bundle Size を削減するために、以下のような手を加えました。
- ESM 形式での提供
- 実装の構造を保ったままでの ESM ファイルの生成
- package.json に
sideEffects=falseを設定
これにより、Bundle Size が大幅に削減されました。
昨今では、Frontend や Edge Worker など、Bundle Size が気になる環境が増えてきています。 Typia はこれらの環境でも安心して使えるようになりました。
ぜひ、お試しください!
余談
作者曰く、Typia は nestia という Library のために作られたものだそうです。
なので、出自が Backend であることがわかります。
Backend 用途では、Bundle Size はあまり気にならないかもしれません。
今回の Bundle Size の削減、および unplugin-typia の開発により、Frontend への導入のハードルが下がり、より多くの人に使ってもらえるようになると嬉しいです。
宣伝 GitHub Sponsorsを始めました
https://github.com/sponsors/ryoppippi/
この度GitHub Sponsorsを始めました。 Typia 、 unplugin-typia を含め、そのほかにも色々 Library 等のメンテナンスをしています。
もしよろしければ、スポンサーになっていただけると嬉しいです!