🗜️

TypiaのBundle Sizeを大幅に削減した話 (65.99 KB -> 2.53 KB)

2024/07/12に公開

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ヶ月前にこんな記事を書きました。

https://zenn.dev/ryoppippi/articles/c4775a3a5f3c11

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 よりも小さくなっていることもわかります。

では、それぞれの 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 を使う際にはこの最適化が効果的です。

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 とは以下のような構文のことです。

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

この問題を解決するために、6.4.0 では rollup の設定を変更し、元の TypeScript ファイルの構造を保ったままで mjs ファイルを生成し、これを提供するようにしました。
具体的には preserveModules: true を有効にして対応しました。
また、そのほかにもいくつかの設定を見直しました。

https://github.com/samchon/typia/pull/1133

(↑めっちゃ必死に必要性を訴えているのがわかる)

6.4.1

6.4.1 では、package.json の sideEffectsfalse に設定しました。

https://github.com/samchon/typia/pull/1146

sideEffects は以下のような設定です。

{
  "sideEffects": false
}

これは、このパッケージが副作用を持たないことを示すものです。

sideEffectsに関しては、Typia のコード内で /*#__PURE__*/ というコメントが至る所で使われているため、設定は不要だと考えていました。

しかし、実際に最小環境を作成して検証をしてみると、sideEffects を設定していない時は、依存関係の一つである ret.js という Library が常に含まれてしまうことがわかりました。
ret.jsTypia の random generator の時にのみ使用される Library であり、それ以外の場面では使用されません。

明示的に sideEffects を設定すると、ret.js が含まれなくなり、Bundle Size がさらに削減されました。

この設定については以下の記事が参考になりました。
https://zenn.dev/uttk/articles/re-export-tree-shaking

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 はこれらの環境でも安心して使えるようになりました。
ぜひ、お試しください!

余談

作者曰く、Typianestia という Library のために作られたものだそうです。

https://nestia.io/

なので、出自が Backend であることがわかります。

Backend 用途では、Bundle Size はあまり気にならないかもしれません。

今回の Bundle Size の削減、および unplugin-typia の開発により、Frontend への導入のハードルが下がり、より多くの人に使ってもらえるようになると嬉しいです。

宣伝 GitHub Sponsorsを始めました

https://github.com/sponsors/ryoppippi/

この度GitHub Sponsorsを始めました。
Typiaunplugin-typia を含め、そのほかにも色々 Library 等のメンテナンスをしています。
もしよろしければ、スポンサーになっていただけると嬉しいです!

Discussion