@ryoppippi

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

11 Jul 2024 ・ 15 min read


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 VersionSimpler SchemaSimpler Schema (Gzip)Large SchemaLarge Schema (Gzip)Notes
6.0.565.99 KiB14.51 KiB74.26 KiB15.43 KiBOnly CJS
6.0.636.47 KiB10.1 KiB44.75 KiB11.03 KiBFirst ESM Support
6.4.06.76 KiB2.69 KiB15.04 KiB3.64 KiBESM with file splitting
6.4.12.53 KiB1.1 KiB10.8 KiB2.06 KiBEnable sideEffects=false
valibot(v0.35.0)4.01 KiB1.43 KiB6.05 KiB1.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 を​使う​際には​この​最適化が​効果的です。

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

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 の​ために​作られた​ものだそうです。

https://nestia.io/

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

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

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

宣伝 GitHub Sponsorsを​始めました

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

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

comment on bluesky / twitter
CC BY-NC-SA 4.0 2022-PRESENT © ryoppippi