Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal(zod-openapi): reduce build/bundle size by ~60kb by only type importing zod #163

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

tefkah
Copy link

@tefkah tefkah commented Sep 11, 2023

Hi!

Love this package, have been using it successfully in several projects, thank you so much for making it!

TLDR: Removing instanceof and just importing zod as types can reduce bundle sizes in some cases by 60kb. I think this is a worthwile gain for relatively little pain.


I've noticed that when used in combination with e.g. ts-rest or anything else that lets this into the bundle the size of the bundle increases dramatically because of the direct dependency on zod.
Of course, anything that lets your packages into the bundle would probably also let zod into it, but because your package also explicitly imports zod/lib/types it confounds the issue.

I noticed that, barring 4 uses of instanceof, almost all the usage of zod in this package is on the type level. If we were able to import type instead of plain import zod, we would be able to drop zod from the js import, which would be nice.

So that is what I did, see below. I rather naively replaced the calls to instanceof, which leads to one test failing (one that checks for z.record(z.unknown()). I don't really know how to fix this, hence this being a draft PR.

See here a comparison of the different bundle sizes, checked by doing npx banal dist/packages/zod-openapi/src/index.js.

I've also tested this in a real world use case (https://github.com/tefkah/pubpub-client/tree/feat/integrate), using the dep from npm vs the built one in this PR. I can go into detail on how to do this, but the method is quite reliable (uses yalc).

As you can see, it saves a cool 60kb, nothing to sneeze at!

Of course, this whole problem can be solved by not including zod schemas in the bundle in the first place, but sometimes this is either unavoidable or desirable (say you want to do client-side validation). Having to separate out the openapi comments can be a bit of a pain, and this PR could be a relatively cheap way save some kbs.

Concrete questions

I've marked the 4 places where instanceof is used and how I attempted to replace them. Could you have a look to see if this makes sense? At least one of them is not a valid replacement, and maybe it is impossible to truly replace instanceof here. Would love your thoughts!

Comparisons

Base package

Before

image

This PR

image

Real world use case (pubpub-client)

Before

image

Using this PR

image

zodRef._def.catchall instanceof z.ZodNever ||
// zodRef._def.catchall instanceof z.ZodNever ||
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assuredly misses some things, how could we check this without using instanceof?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually don't think this misses things, if X is instanceof Y, it will definitely have the same typename.
Of course, things could theoretically have the same typename but not be the same zod type, but looking at zod this seems unlikely to happen.

Comment on lines 224 to 229
item instanceof z.ZodDefault ||
// item instanceof z.ZodDefault ||
item._def.typeName === 'ZodDefault'
) && !(item instanceof z.ZodNever || item._def.typeName === 'ZodDefault')
) && !(
// item instanceof z.ZodNever ||
item._def.typeName === 'ZodNever' ||
item._def.typeName === 'ZodDefault')
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this works?

Comment on lines 260 to 265
zodRef._def.valueType instanceof z.ZodUnknown
// @ts-expect-error This comparison appears to be unintentional because the types 'ZodFirstPartyTypeKind.ZodRecord' and '"ZodUnknown"' have no overlap.ts(2367)
zodRef._def.typeName === 'ZodUnknown'
// zodRef._def.valueType instanceof z.ZodUnknown
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one definitely does not work; zodRef._def.typeName does not work like this, I've had to ts-expect-error it. Do you know how to fix this?

@nx-cloud
Copy link

nx-cloud bot commented Sep 11, 2023

☁️ Nx Cloud Report

CI is running/has finished running commands for commit 3dd903b. As they complete they will appear below. Click to see the status, the terminal output, and the build insights.

📂 See all runs for this branch


✅ Successfully ran 2 targets

Sent with 💌 from NxCloud.

@tefkah tefkah changed the title Proposal: drop zod dependency in bundle by only importing zod types Proposal: reduce build/bundle size by ~60kb by only type importing zod Sep 11, 2023
@tefkah tefkah changed the title Proposal: reduce build/bundle size by ~60kb by only type importing zod Proposal(zod-openapi): reduce build/bundle size by ~60kb by only type importing zod Sep 11, 2023
@tefkah
Copy link
Author

tefkah commented Sep 20, 2023

I managed to fix the test.

@tefkah tefkah marked this pull request as ready for review September 20, 2023 21:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant