Open fabian-hiller opened 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.
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.
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.
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();
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.
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!
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).
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
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.
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
@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?
So like dist/validation/error/typia.js
includes ../../data
which cannot bundle on bundlejs
because of external file
You can remove the data and replace it with null
.
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.
@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
Do you know why Valibot is smaller than Typia? Were you able to remove unused code imported from Typia?
idk, but tree-shaking is enough, maybe I'll investigate it
So I found that whole ret.js
implementation is bundled even tho we do not use it...
Hmm, because of rollup config??? idk
How is it imported? Is ret.js
tree shakable?
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);
Thanks for your research! It's good to know. I think your improvements add a lot of value to Typia.
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.
@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
@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
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 |
@fabian-hiller Thank you so much for your benchmark repository. It helps me a lot.
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.