--- title: "TypiaのBundle Sizeを大幅に削減した話 (65.99 KB -> 2.53 KB)" date: '2024-07-11' isPublished: true lang: 'ja' --- # 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が大きすぎる問題 さて、[上にあげた記事の最後の部分](/blog/2024-06-12-zenn-c4775a3a5f3c11-ja)で、Typia の Bundle Size が大きい問題があると書きました。 実際、[Valibotの作者である Fabian Hiller 氏の論文](https://valibot.dev/thesis.pdf) によると、`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 氏の論文](https://valibot.dev/thesis.pdf)が行った実装をベースに以下のような手を加えたものを使用しました。 - 論文に使われた 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`](https://github.com/ryoppippi/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](https://typia.io/docs/random/) を使う際にはこの最適化が効果的です。 https://github.com/samchon/typia/pull/1099
## `6.4.0` `6.4.0` では、ESM において、TypeScript のファイルの構造を保ったままで mjs ファイルを生成するよう `rollup` の設定を変更しました。 それ以前の`rollup`の設定では、ビルド時に全ての TypeScript ファイルを一つの `index.mjs` にまとめていました。 これでも問題ないだろうと考えていましたが、実際にはファイル分割を行うことで、Tree-shaking が効果的になることがわかりました。 なぜこのような Bundle Size の差が出たのかというと、Typia 内部で namespace import が多用されていたためです。 元々、`Typia` の内部では namespace が多用されていました。 namespace とは以下のような構文のことです。 ```typescript 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 とは以下のような構文のことです。 ```typescript 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` > > ```typescript > import * as A from './A'; > > console.log(A.a); > ``` > > `A.ts` > > ```typescript > export const a = 1; > export const b = 2; > ``` > > のようなコードがあったと仮定します。 > > 理想的には、これらをビルドした時、`A.b` は使われていないので、`A.b` が含まれないようにしたいです。 > > `index.mjs` > > ```javascript > import {'\{'} a {'\}'} from './A'; > > console.log(a); > ``` > > `A.mjs` > > ```javascript > export const a = 1; > ``` > > しかし、これが Library としての compile 時に一つのファイルにまとめられてしまうと、`A` という namespace が一つの object にまとめられてしまいます。 > > `index.mjs` > > ```javascript > 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` は以下のような設定です。 ```json { "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 のために作られたものだそうです。 https://nestia.io/ なので、出自が Backend であることがわかります。 Backend 用途では、Bundle Size はあまり気にならないかもしれません。 今回の Bundle Size の削減、および [`unplugin-typia`](https://github.com/ryoppippi/unplugin-typia) の開発により、Frontend への導入のハードルが下がり、より多くの人に使ってもらえるようになると嬉しいです。 # 宣伝 GitHub Sponsorsを始めました https://github.com/sponsors/ryoppippi/ この度GitHub Sponsorsを始めました。 `Typia` 、 `unplugin-typia` を含め、そのほかにも色々 Library 等のメンテナンスをしています。 もしよろしければ、スポンサーになっていただけると嬉しいです!