samchon / typia

Super-fast/easy runtime validators and serializers via transformation
https://typia.io/
MIT License
4.45k stars 153 forks source link

Why does Typia initial have a bundle size of 7.6 kB? #752

Open fabian-hiller opened 1 year ago

fabian-hiller commented 1 year ago

Hi Jeongho, my name is Fabian, and I am the author of the schema validation library Valibot. I am currently investigating different schema libraries for my bachelor's thesis. Typia is interesting because the approach is completely different.

While examining your library, I noticed that regardless of the complexity of a schema, the bundle size starts at 7.6 kB due to the exported code of the type package. What exactly is included in this 7.6 kB? I have seen that this code is used, for example, when validating an email.

Shouldn't it be theoretically possible to import only the code that is actually needed by the compile step? This would make the bundle size of Typia start initially at 0 KB.

samchon commented 1 year ago

I have not measured exact bundle size, but the reason why not 0kb may be one of them below:

At 1st, lots of types embeded in typia. Looking at below files, then you may understand what I mean.

Also, there're utility functions used in transformed codes.

At last, typia imports randexp library for random regex paterrn data generation.

fabian-hiller commented 1 year ago

The types should have no influence on the JavaScript bundle size, and if you export and import the individual functions separately instead of using a default export, you can probably reduce the bundle size for most schemas to 0 kB apart from the generated code. This brings a significant advantage when Typia is used in the front end, for example, with forms. However, if that is not your goal, I understand your approach.

samchon commented 1 year ago

To accomplish 0kb bundling size, I've to change typia to call require() function only when needed.

It's not any problem in CommonJS, but such approach would be a critical problem in ESM.

fabian-hiller commented 1 year ago

I think that this also works with ESM. When using wildcard imports, tree shaking and code splitting can be applied by a bundler such as esbuild. If you're interested, you can try it out on Bundlejs with Valibot. Change the import statement to a wildcard and call e.g. the string function.

export * as v from "valibot";

v.string();
samchon commented 1 year ago

Adding import statement on top of document is not easy thing for current typia, because typia transforms only the function call statements of typia. Considering merits of zero bundling, its priority is lower for me. Of course, if someone sends a PR about it, I always welcome.

fabian-hiller commented 1 year ago

The import statement already exists. It is added by the user as it is currently. I had created the issue only in the context of my thesis. You don't need to implement it. Thank you very much for the exchange. Typia is an exciting validation library!

colecrouter commented 1 year ago

I made a fork that builds Typia as ESM instead of CJS, which would resolve this issue (untested). However, TS-patch currently doesn't seem to fully support ESM plugins, so the Typia plugin would need to exist as a separate CJS package.

I'm not sure if my assertation is correct, but I have made a comment on this issue about it. At the very least, this could be a reason for why 0kb is not possible (for now).

EloB commented 7 months ago

I've create draft PR that includes some optimizations for tree shaking. I'm not complete yet but all tests passes. https://github.com/samchon/typia/pull/928

ryoppippi commented 2 months ago

We added ESM build support in recent PRs.

While examining your library, I noticed that regardless of the complexity of a schema, the bundle size starts at 7.6 kB due to the exported code of the type package. What exactly is included in this 7.6 kB? I have seen that this code is used, for example, when validating an email.

@fabian-hiller Could you do the bundle size experiments again? Or could you show your code for this experiments.

fabian-hiller commented 2 months ago

I basically created a Typia schema and used it by calling typia.validate<TypiaSchema>(...). After compiling the code, you can check the output to investigate its bundle size.

See my bundle size comparison on page 62: https://valibot.dev/thesis.pdf

ryoppippi commented 2 months ago

@fabian-hiller Thanks! In this repo, you use tsc for building, right? Whish bundler did you use for thesis? Do you have a code for bundling?

fabian-hiller commented 2 months ago

I think I copied the output from tsc into bundlejs which used esbuild.

ryoppippi commented 2 months ago

So like dist/validation/error/typia.js includes ../../data which cannot bundle on bundlejs because of external file

fabian-hiller commented 2 months ago

You can remove the data and replace it with null.

ryoppippi commented 2 months ago

Ok so I'm bundling with bundlejs and found that re-exported named import is not tree-shaked in esbuild. I'll investigate with rollup.

ryoppippi commented 2 months ago

@fabian-hiller I added rollup build config, update typia and valibot. Then compare sizes.

https://github.com/ryoppippi/thesis-benchmarks/tree/feature/update-and-rollup

fabian-hiller commented 2 months ago

Do you know why Valibot is smaller than Typia? Were you able to remove unused code imported from Typia?

ryoppippi commented 2 months ago

idk, but tree-shaking is enough, maybe I'll investigate it

ryoppippi commented 2 months ago

So I found that whole ret.js implementation is bundled even tho we do not use it...

Hmm, because of rollup config??? idk

fabian-hiller commented 2 months ago

How is it imported? Is ret.js tree shakable?

ryoppippi commented 2 months ago

https://github.com/samchon/typia/pull/1146 @fabian-hiller I added sideEffect=false to package.json and ret.js has gone lol. idk why

After patching typia to add sideEffect, the result is here:

However still valibot is smaller Library Success Size Success Size (gzip) Error Size Error Size (gzip)
arktype 48.3 kB 16.2 kB 48.2 kB 16.2 kB
typia 12.4 kB 2.69 kB 12.3 kB 2.69 kB
valibot 7.56 kB 2.53 kB 7.42 kB 2.53 kB
zod 55.9 kB 13.5 kB 55.7 kB 13.5 kB

In typia, if a similar format is specified for another type, it is used as is, regardless of whether it has been used before. In valibot, on the other hand, the function is imported as a function, so it does not contain multiple implementations of the same function.

            ((input, _path, _exceptionable = true) => {
                const $vo0 = (input, _path, _exceptionable = true) => ["number" === typeof input.id || $report(_exceptionable, {
                        path: _path + ".id",
                        expected: "number",
                        value: input.id
                    }), input.created instanceof Date || $report(_exceptionable, {
                        path: _path + ".created",
                        expected: "Date",
                        value: input.created
                    }), "string" === typeof input.title && (1 <= input.title.length || $report(_exceptionable, {
                        path: _path + ".title",
                        expected: "string & MinLength<1>",
                        value: input.title
                    })) && (input.title.length <= 100 || $report(_exceptionable, {
                        path: _path + ".title",
                        expected: "string & MaxLength<100>",
                        value: input.title
                    })) || $report(_exceptionable, {
                        path: _path + ".title",
                        expected: "(string & MinLength<1> & MaxLength<100>)",
                        value: input.title
                    }), "string" === typeof input.brand && (1 <= input.brand.length || $report(_exceptionable, {
                        path: _path + ".brand",
                        expected: "string & MinLength<1>",
                        value: input.brand
                    })) && (input.brand.length <= 30 || $report(_exceptionable, {
                        path: _path + ".brand",
                        expected: "string & MaxLength<30>",
                        value: input.brand
                    })) || $report(_exceptionable, {
                        path: _path + ".brand",
                        expected: "(string & MinLength<1> & MaxLength<30>)",
                        value: input.brand
                    }), "string" === typeof input.description && (1 <= input.description.length || $report(_exceptionable, {
                        path: _path + ".description",
                        expected: "string & MinLength<1>",
                        value: input.description
                    })) && (input.description.length <= 500 || $report(_exceptionable, {
                        path: _path + ".description",
                        expected: "string & MaxLength<500>",
                        value: input.description
                    })) || $report(_exceptionable, {
                        path: _path + ".description",
                        expected: "(string & MinLength<1> & MaxLength<500>)",
                        value: input.description
                    }), null === input.price || "number" === typeof input.price && (1 <= input.price || $report(_exceptionable, {
                        path: _path + ".price",
                        expected: "number & Minimum<1>",
                        value: input.price
                    })) && (input.price <= 10000 || $report(_exceptionable, {
                        path: _path + ".price",
                        expected: "number & Maximum<10000>",
                        value: input.price
                    })) || $report(_exceptionable, {
                        path: _path + ".price",
                        expected: "((number & Minimum<1> & Maximum<10000>) | null)",
                        value: input.price
                    }), null === input.discount || "number" === typeof input.discount && (1 <= input.discount || $report(_exceptionable, {
                        path: _path + ".discount",
                        expected: "number & Minimum<1>",
                        value: input.discount
                    })) && (input.discount <= 100 || $report(_exceptionable, {
                        path: _path + ".discount",
                        expected: "number & Maximum<100>",
                        value: input.discount
                    })) || $report(_exceptionable, {
                        path: _path + ".discount",
                        expected: "((number & Minimum<1> & Maximum<100>) | null)",
                        value: input.discount
                    }), "number" === typeof input.quantity && (1 <= input.quantity || $report(_exceptionable, {
                        path: _path + ".quantity",
                        expected: "number & Minimum<1>",
                        value: input.quantity
                    })) && (input.quantity <= 10 || $report(_exceptionable, {
                        path: _path + ".quantity",
                        expected: "number & Maximum<10>",
                        value: input.quantity
                    })) || $report(_exceptionable, {
                        path: _path + ".quantity",
                        expected: "(number & Minimum<1> & Maximum<10>)",
                        value: input.quantity
                    }), (Array.isArray(input.tags) || $report(_exceptionable, {
                        path: _path + ".tags",
                        expected: "(Array<string> & MinItems<1> & MaxItems<30>)",
                        value: input.tags
                    })) && ((1 <= input.tags.length || $report(_exceptionable, {
                        path: _path + ".tags",
                        expected: "Array<> & MinItems<1>",
                        value: input.tags
                    })) && (input.tags.length <= 30 || $report(_exceptionable, {
                        path: _path + ".tags",
                        expected: "Array<> & MaxItems<30>",
                        value: input.tags
                    })) && input.tags.map((elem, _index1) => "string" === typeof elem || $report(_exceptionable, {
                        path: _path + ".tags[" + _index1 + "]",
                        expected: "string",
                        value: elem
                    })).every(flag => flag)) || $report(_exceptionable, {
                        path: _path + ".tags",
                        expected: "(Array<string> & MinItems<1> & MaxItems<30>)",
                        value: input.tags
                    }), (Array.isArray(input.images) || $report(_exceptionable, {
                        path: _path + ".images",
                        expected: "Array<__type>",
                        value: input.images
                    })) && input.images.map((elem, _index2) => ("object" === typeof elem && null !== elem || $report(_exceptionable, {
                        path: _path + ".images[" + _index2 + "]",
                        expected: "__type",
                        value: elem
                    })) && $vo1(elem, _path + ".images[" + _index2 + "]", _exceptionable) || $report(_exceptionable, {
                        path: _path + ".images[" + _index2 + "]",
                        expected: "__type",
                        value: elem
                    })).every(flag => flag) || $report(_exceptionable, {
                        path: _path + ".images",
                        expected: "Array<__type>",
                        value: input.images
                    }), (Array.isArray(input.ratings) || $report(_exceptionable, {
                        path: _path + ".ratings",
                        expected: "Array<__type>.o1",
                        value: input.ratings
                    })) && input.ratings.map((elem, _index3) => ("object" === typeof elem && null !== elem || $report(_exceptionable, {
                        path: _path + ".ratings[" + _index3 + "]",
                        expected: "__type.o7",
                        value: elem
                    })) && $vo2(elem, _path + ".ratings[" + _index3 + "]", _exceptionable) || $report(_exceptionable, {
                        path: _path + ".ratings[" + _index3 + "]",
                        expected: "__type.o7",
                        value: elem
                    })).every(flag => flag) || $report(_exceptionable, {
                        path: _path + ".ratings",
                        expected: "Array<__type>.o1",
                        value: input.ratings
                    })].every(flag => flag);
                const $vo1 = (input, _path, _exceptionable = true) => ["number" === typeof input.id || $report(_exceptionable, {
                        path: _path + ".id",
                        expected: "number",
                        value: input.id
                    }), input.created instanceof Date || $report(_exceptionable, {
                        path: _path + ".created",
                        expected: "Date",
                        value: input.created
                    }), "string" === typeof input.title && (1 <= input.title.length || $report(_exceptionable, {
                        path: _path + ".title",
                        expected: "string & MinLength<1>",
                        value: input.title
                    })) && (input.title.length <= 100 || $report(_exceptionable, {
                        path: _path + ".title",
                        expected: "string & MaxLength<100>",
                        value: input.title
                    })) || $report(_exceptionable, {
                        path: _path + ".title",
                        expected: "(string & MinLength<1> & MaxLength<100>)",
                        value: input.title
                    }), "jpg" === input.type || "png" === input.type || $report(_exceptionable, {
                        path: _path + ".type",
                        expected: "(\"jpg\" | \"png\")",
                        value: input.type
                    }), "string" === typeof input.url && (/^(?:https?|ftp):\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)(?:\.(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu.test(input.url) || $report(_exceptionable, {
                        path: _path + ".url",
                        expected: "string & Format<\"url\">",
                        value: input.url
                    })) || $report(_exceptionable, {
                        path: _path + ".url",
                        expected: "(string & Format<\"url\">)",
                        value: input.url
                    })].every(flag => flag);
                const $vo2 = (input, _path, _exceptionable = true) => ["number" === typeof input.id || $report(_exceptionable, {
                        path: _path + ".id",
                        expected: "number",
                        value: input.id
                    }), "number" === typeof input.stars && (1 <= input.stars || $report(_exceptionable, {
                        path: _path + ".stars",
                        expected: "number & Minimum<1>",
                        value: input.stars
                    })) && (input.stars <= 5 || $report(_exceptionable, {
                        path: _path + ".stars",
                        expected: "number & Maximum<5>",
                        value: input.stars
                    })) || $report(_exceptionable, {
                        path: _path + ".stars",
                        expected: "(number & Minimum<1> & Maximum<5>)",
                        value: input.stars
                    }), "string" === typeof input.title && (1 <= input.title.length || $report(_exceptionable, {
                        path: _path + ".title",
                        expected: "string & MinLength<1>",
                        value: input.title
                    })) && (input.title.length <= 100 || $report(_exceptionable, {
                        path: _path + ".title",
                        expected: "string & MaxLength<100>",
                        value: input.title
                    })) || $report(_exceptionable, {
                        path: _path + ".title",
                        expected: "(string & MinLength<1> & MaxLength<100>)",
                        value: input.title
                    }), "string" === typeof input.text && (1 <= input.text.length || $report(_exceptionable, {
                        path: _path + ".text",
                        expected: "string & MinLength<1>",
                        value: input.text
                    })) && (input.text.length <= 1000 || $report(_exceptionable, {
                        path: _path + ".text",
                        expected: "string & MaxLength<1000>",
                        value: input.text
                    })) || $report(_exceptionable, {
                        path: _path + ".text",
                        expected: "(string & MinLength<1> & MaxLength<1000>)",
                        value: input.text
                    }), (Array.isArray(input.images) || $report(_exceptionable, {
                        path: _path + ".images",
                        expected: "Array<__type>.o2",
                        value: input.images
                    })) && input.images.map((elem, _index4) => ("object" === typeof elem && null !== elem || $report(_exceptionable, {
                        path: _path + ".images[" + _index4 + "]",
                        expected: "__type.o14",
                        value: elem
                    })) && $vo3(elem, _path + ".images[" + _index4 + "]", _exceptionable) || $report(_exceptionable, {
                        path: _path + ".images[" + _index4 + "]",
                        expected: "__type.o14",
                        value: elem
                    })).every(flag => flag) || $report(_exceptionable, {
                        path: _path + ".images",
                        expected: "Array<__type>.o2",
                        value: input.images
                    })].every(flag => flag);
                const $vo3 = (input, _path, _exceptionable = true) => ["number" === typeof input.id || $report(_exceptionable, {
                        path: _path + ".id",
                        expected: "number",
                        value: input.id
                    }), input.created instanceof Date || $report(_exceptionable, {
                        path: _path + ".created",
                        expected: "Date",
                        value: input.created
                    }), "string" === typeof input.title && (1 <= input.title.length || $report(_exceptionable, {
                        path: _path + ".title",
                        expected: "string & MinLength<1>",
                        value: input.title
                    })) && (input.title.length <= 100 || $report(_exceptionable, {
                        path: _path + ".title",
                        expected: "string & MaxLength<100>",
                        value: input.title
                    })) || $report(_exceptionable, {
                        path: _path + ".title",
                        expected: "(string & MinLength<1> & MaxLength<100>)",
                        value: input.title
                    }), "jpg" === input.type || "png" === input.type || $report(_exceptionable, {
                        path: _path + ".type",
                        expected: "(\"jpg\" | \"png\")",
                        value: input.type
                    }), "string" === typeof input.url && (/^(?:https?|ftp):\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)(?:\.(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu.test(input.url) || $report(_exceptionable, {
                        path: _path + ".url",
                        expected: "string & Format<\"url\">",
                        value: input.url
                    })) || $report(_exceptionable, {
                        path: _path + ".url",
                        expected: "(string & Format<\"url\">)",
                        value: input.url
                    })].every(flag => flag);
                return ("object" === typeof input && null !== input || $report(true, {
                    path: _path + "",
                    expected: "TypiaSchema",
                    value: input
                })) && $vo0(input, _path + "", true) || $report(true, {
                    path: _path + "",
                    expected: "TypiaSchema",
                    value: input
                });
            })(input, "$input", true);
fabian-hiller commented 2 months ago

Thanks for your research! It's good to know. I think your improvements add a lot of value to Typia.

samchon commented 2 months ago

There had been interesting conversations at last night.

Always thanks to @ryoppippi and thanks for interesting thesis @fabian-hiller.

I'll remove every embedded namespaced functions of typia functions like assert() and json.stringify() in the next v7 major update. Instead, typia v7 will write import statements only when required about the embedded functions. If the v7 major update be released, bundled size of typia would reached to 0.

ryoppippi commented 2 months ago

@fabian-hiller

https://github.com/ryoppippi/thesis-benchmarks/tree/feature/update-and-rollup

I updated a bundle size result after v6.4.1 has released. If you are interested in, please check this out

ryoppippi commented 2 months ago

@samchon @fabian-hiller Also you can check this result at ryoppippi/thesis-benchmarks@feature/update-and-rollup

EDIT:I seem to have remembered incorrectly when I supported ESM, so updated the table

Bundle Size Comparison between Typia's version

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(0.35.0) 4.01 KiB 1.43 KiB 6.05 KiB 1.89 KiB
ryoppippi commented 2 months ago

@fabian-hiller Thank you so much for your benchmark repository. It helps me a lot.