【Zod】v4.5の新機能とパフォーマンス改善をまとめて解説

はじめに
この記事の概要
こんにちは、株式会社TOKOSのスギタです!
Zod v4.5が2026年8月28日にリリースされました。
新機能とパフォーマンス改善、そしてアップグレードで挙動が変わる箇所をまとめます。
Zod 4.5
Zod 4.5 is now available, with z.compile() ahead-of-time compilation, up to 9x less memory per schema, and a batch of long-awaited features.
zod.dev対象読者
- すでにZodを業務で使っているTypeScript開発者の方
- v4系を使っていて、v4.5へのアップグレードを検討している方
z.compile()を自分のプロジェクトで使えるか判断したい方
この記事で扱う内容、扱わない内容
扱う内容は下記です。
- Zod v4.5で追加された機能
- v4.4からのパフォーマンス改善点
- アップグレードで挙動が変わる箇所
扱わない内容は下記です。
- Zod自体の入門(スキーマ定義や
parseの基本) - Zod v3からv4への移行
- Zod Mini(Zod本体の軽量版)の詳細(
z.compile()のバンドルサイズに関わる部分だけ触れます) react-hook-formなど他ライブラリとの連携
Zod v4.5で入った変更の全体像
新機能の追加とパフォーマンスの改善、そして仕様をより正確にする破壊的変更が入っています。
その中でも気になるものだけ取り上げます!
| 変更点 | 内容 |
|---|---|
z.compile() | スキーマの検証を1本のJavaScript関数に書き出しておく新機能。キーの多いオブジェクトでparseが最大10.2倍速い |
| メモリ削減と失敗時の高速化 | 内部構造の見直しによる改善で、コードの書き換えは不要。スキーマ1つが使うメモリが最大9.8分の1、失敗するsafeParse()は7.6倍速い |
z.validate() | 値が通るかどうかだけを真偽値で返す型ガード。エラーを組み立てない分、不正な入力に対して.safeParse().successより最大16倍速い |
z.toZod() | 先に決めた型とスキーマが一致するか検証。satisfies z.ZodType<T>では見逃していた余分なキーやz.any()を弾ける |
| 文字列長がコードポイント単位に | .min()と.max()の数え方がUTF-16のコードユニットからUnicodeのコードポイントへ変わり、絵文字を含む入力で結果が変わる |
ここに入らなかった追加分は「その他の新機能」でまとめて扱います。
z.deepPartial()やz.creditCard()、循環参照を含むデータのparseなどが該当します。
z.compile()
Zodはキーが増えるほど、parseに時間がかかります。
z.compile()は、この作業を先に済ませて1本のJavaScript関数に書き出しておく機能です。
import * as z from "zod"
const Player = z.object({
username: z.string(),
bio: z.string(),
xp: z.number(),
})
const CompiledPlayer = z.compile(Player)
CompiledPlayer.parse({ username: "billie", bio: "hello", xp: 100 })z.compile()が返すのは元のスキーマと同じ型です。
呼び出し側のコードは変わりません。
使う上で押さえておきたいのは次の2点です!
- エラーメッセージはコンパイルの有無で変わらない
- 失敗するケースは速くならない
コンパイル済みの関数は、値が不正だと分かった時点で処理をやめます。
エラーを組み立てるのが元のパーサなので、メッセージは今までと変わりません。
書き出されるコードの中身は、公式ドキュメントを参照してください。
AOT compilation | Zod
Ahead-of-time schema compilation for hot validation paths
zod.dev速度は公式のベンチマークが公開されています。
| スキーマ | 標準 | コンパイル済み |
|---|---|---|
| 20キーのオブジェクト | 301ns | 38ns |
| 10要素のオブジェクト配列 | 377ns | 68ns |
| 10要素の文字列配列 | 241ns | 33ns |
3つのオブジェクトのunion | 190ns | 36ns |
5キーのstrictObject | 117ns | 32ns |
discriminatedUnion | 92ns | 27ns |
キーが多いほど差が開きます!
50キーのオブジェクトでは10.2倍という数値も出ています。
コンパイルはチェーンの最後に
.refine()や.extend()、.optional()が返すスキーマはコンパイルされていません。
チェーンの途中でz.compile()を呼ぶと、その後に足したものは元のパーサで処理されます。
const bad = z.compile(z.string()).refine((val) => val.length > 1)
const good = z.compile(z.string().refine((val) => val.length > 1)) コンパイルできないスキーマ
すべてのスキーマがコンパイルできるわけではありません!
対応していない機能を含んでいても、z.compile()はエラーを投げずにそのまま返します。
{ strict: true }を渡しておくと、そのときにエラーを投げてくれます。
z.compile(z.coerce.number(), { strict: true })ZodCompileUnsupportedError: z.compile does not support coercion (z.coerce.number());
this schema must use the runtime parser対応していないのは次のものです。
- 非同期の
refine・transform・check z.xor()- 再帰スキーマ
z.coerce.*whenを指定したcheck- コールバックを渡した
.catch()
.catch("fallback")のように定数を渡す形はコンパイルできます。
非対応のものが混ざっていても、必ずスキーマ全体が諦められるわけではありません。
効く範囲は場所によって変わります。
object・array・tuple・record・intersectionの中なら、非対応のものだけが元のパーサに回り、外側はコンパイルされたままunionのメンバーに非対応のものがある場合、コールバック形式の.catch()、どこかに非同期処理がある場合は全体が元のパーサに戻る
バンドルサイズのトレードオフ
コンパイラそのものが、それなりの量のコードでできています。
z.compile()を呼ぶかzod/compileをimportした時点で、その分がバンドルに乗ります。
| バンドル | compileなし | compileあり |
|---|---|---|
| Zod | 24.1KB | 31.1KB |
| Zod Mini | 4.6KB | 13.2KB |
いずれもgzip後の数値です。
Zodでは約1.3倍増加ですが、Zod Miniでは2.9倍になります。
Zod Miniを選ぶ理由はサイズなので、Miniとcompileの組み合わせは噛み合いません!
一度も呼ばなければビルド時に丸ごと削られるので、使わないなら増加はゼロです!
サーバーサイドならバンドルサイズを気にせず採用できます。
クライアントで動かすバリデーションなら、7KB分の速度が得られるかを先に確かめたいところです。

ZodMiniの4.6KBが13.2KBになるのは、正直かなり厳しい数字だと感じました。Miniとcompileはどちらかひとつでいいという解釈をしています。
下記公式ブログで紹介されています!
Introducing z.compile()
z.compile() walks a schema once and produces a flat, loop-free JavaScript validator that runs far faster than the standard parser. Arrays, unions, and nested or wide objects parse 3 to 7x faster, with identical output and identical errors.
zod.devimport "zod/compile"で全体に適用する
スキーマごとに毎回1つずつコンパイルするのでは無く、アプリケーション全体でコンパイルを有効にできます。
import "zod/compile" // スキーマを定義するモジュールより前に置く
import * as z from "zod"
const schema = z.object({ name: z.string() })
schema.parse({ name: "ok" }) // 初回のparseでコンパイルされるimport "zod/compile"は読み込む順番が重要です!
これより後に定義されたスキーマだけが、コンパイルの対象になります。
import文の並び順で保証しきれないときは、Node.jsの起動時に読み込みます。
node --import zod/compile app.jsコンパイルされるのは、実際にparseしたスキーマだけです。
定義しただけで使っていないスキーマは対象外になります。
公式ドキュメントは「This import is for applications, not libraries.」と明記しています。
ライブラリから読み込むと、利用側のスキーマまで巻き込むためです。
CSP(Content Security Policy)でnew Functionを使えない環境では、コンパイルに失敗してそのまま元のパーサで動きます。
Cloudflare Workersなどが当てはまります。
はっきり無効にしたいときはz.config({ jitless: true })を設定してください。
何もしなくても速くなった部分
z.compile()は自分で呼んだときだけ効く機能です。
一方で、v4.5にはコードを変えなくても効く改善が入っています。
スキーマ1個あたりのメモリが最大9.8倍削減
スキーマ1つが使うメモリの比較です。
| スキーマ | v4.4.3 | v4.5 |
|---|---|---|
| 10キーのオブジェクト | 82.0KB | 11.0KB |
| union | 17.5KB | 2.13KB |
z.string().min(1) | 16.7KB | 3.37KB |
| record | 16.4KB | 2.64KB |
z.string().optional() | 12.6KB | 1.50KB |
z.string() | 7.53KB | 784B |
z.number() | 4.44KB | 706B |
メソッドの持ち方を変えたことによる効果のようです。
これまではスキーマを作った時点で、すべてのメソッドを1つずつ持たせていました。
v4.5では、実際に呼ばれたメソッドだけを後から持たせます。
一度も呼ばないメソッドの分は、まるごと不要になります!
書き方への影響はないので、次のような分割代入も引き続き動きます。
const { parse } = z.string()
parse("some data")Reducing Zod's memory footprint by an order of magnitude with method memoization
A bare z.string() retained 7.5kb of heap in Zod 4.4. In Zod 4.5 it retains 784 bytes. One change to where a schema's methods live accounts for nearly all of it.
zod.devsafeParseの失敗が7.6倍速く
.safeParse()は失敗するたびにZodErrorを作り、スタックトレースまで取っていました。
v4.5でもZodError自体はその場で作られますが、重い処理が後ろにずれました。
エラー文面の組み立てはmessageを読むまで走らず、safeParse()ではスタックの記録も省かれます。
parse()が投げる場合はスタックが必要なので、そちらは今までどおり記録されます。
公式の計測では、失敗するsafeParseが6.3msから840nsになっています!
フォームのように入力途中で何度も失敗する画面なら、成功より失敗のほうが回数は多いはずです。
z.validate()
値が通るかどうかだけを知りたい場面で、.safeParse().successを使っていた方は多いはずです。
この書き方だと、失敗するたびにエラーオブジェクトを組み立てる処理が走ります。
v4.5で追加されたz.validate()は真偽値だけを返します。
z.validate(z.string(), "hi") // true
z.validate(z.string(), 42) // false公式によると、不正な入力に対して.safeParse().successより最大16倍速くなります。
z.compile()を通したスキーマを渡すと、差はさらに開きます。
コンパイル時に真偽値だけを返す判定用の関数も作られていて、z.validate()はそれをそのまま呼ぶためです。
値を組み立てる処理を通らない分だけ速く終わります。
注意したいのは、絞り込まれる型です。
const Age = z.string().transform((s) => Number(s))
const value: unknown = "3"
if (z.validate(Age, value)) {
// ここでの value は string
// transform 後の number ではない
}z.validate()が絞り込むのは、変換した後の型ではなく変換する前の型です!
Zodの用語でいえばoutput<T>ではなくinput<T>側になります。
非同期のrefineやtransformを含む場合はz.validateAsync()を使います。
TypeScriptでは非同期の型ガードを書けないため、こちらはPromise<boolean>を返すだけで型は絞り込まれません。

z.validate()はoutput<T>ではなくinput<T>に対する型ガードです。そのため、transformを挟んだスキーマで使うときは、絞り込まれた型を一度確認しておくと安全です。
z.toZod()
先に型を定義して、それに合うスキーマを書く場面があります。
例えば、外部APIの型や共有パッケージの型に合わせてバリデーションを足すときなどです。
satisfiesでは防げないケース
これまでよく使われていたのはsatisfies z.ZodType<T>です。
ただしこれは「型として当てはまるかどうか」しか見ません。
必須キーの書き忘れは捕まえますが、余分なキーやz.any()はすり抜けてしまいます。
type Player = { username: string; xp: number }
// 型に存在しない admin を足してもエラーにならない
z.object({
username: z.string(),
xp: z.number(),
admin: z.boolean(),
}) satisfies z.ZodType<Player>
// z.any() でも通ってしまう
z.any() satisfies z.ZodType<Player>この2つは手元のtscでもエラーになりませんでした!
z.toZod()なら弾ける
v4.5のz.toZod()は、型とスキーマがぴったり一致するかどうかを見ます。
2段階に分けて呼ぶ点だけ注意してください。
// 1段目: 合わせたい型を渡す
// 2段目: その型と一致するスキーマだけ受け付ける
const PlayerSchema = z.toZod<Player>()(
z.object({
username: z.string(),
xp: z.number(),
admin: z.boolean(), // Playerに無いキー → 型エラー
}),
)
// z.any()も「Playerと一致しない」のでエラー
const AnyPlayer = z.toZod<Player>()(z.any())先ほどと同じ内容ですが、今度は両方エラーになります!
error TS2345: Argument of type 'ZodObject<{ username: ZodString; xp: ZodNumber;
admin: ZodBoolean; }, $strip>' is not assignable to parameter of type
'$ZodObject<{ username: ZodString; xp: ZodNumber;
admin: ToZodKeyMismatch<never, boolean>; }, $strip>'.
Property '"types do not match"' is missing in type '_ZodBoolean<...>'
error TS2345: Argument of type 'ZodAny' is not assignable to parameter of type
'ZodAny & ToZodMismatch<Player, ZodAny>'.
Property '"types do not match"' is missing in type '_ZodType<$ZodAnyInternals>'"types do not match"という文字列がエラーに現れるので、原因を追いやすくなっています。
adminというキー名まで出るのも助かります。
スキーマが正しい場合、z.toZod()は渡したスキーマをそのまま返します。
戻り値の.shapeもそのまま使えます。

z.toZod<Player>()(schema)という2段呼び出しは、初見だと書き間違いに見えます。それでもsatisfiesがz.any()を弾けないと知ってしまうと、型定義に追従させたいスキーマはこちらへ寄せたくなりました。
その他の新機能
z.deepPartial()とexactPartial()
z.deepPartial()はZod 3にあったメソッドです。
v4でいったん削除され、v4.5で関数の形になって戻ってきました。
ネストしたオブジェクトのプロパティも再帰的にoptionalにします。
const Post = z.object({
title: z.string(),
author: z.object({ name: z.string(), email: z.string() }),
})
const PartialPost = z.deepPartial(Post)
// { title?: string; author?: { name?: string; email?: string } }
PartialPost.parse({ author: {} }) // ✅元のスキーマは変更されず、結果もZodObjectのままなので.shapeや.extend()が使えます。
ただし、どんなオブジェクトにも使えるわけではありません。
内部で.partial()を使うため、.refine()を持つオブジェクトに行き当たると例外を投げます。
.partial() cannot be used on object schemas containing refinements既存のスキーマに使うなら、.refine()はz.deepPartial()のあとに付けてください。
discriminatedUnionに使うと、判別に使うキーまでoptionalになります。
判別できなくなるため、結果は通常のunionに変わります。
.exactPartial()は、各フィールドをz.optional()ではなくz.exactOptional()で包みます。
キーの省略は許しますが、はっきりundefinedを渡すと弾きます。
const Recipe = z.object({ title: z.string(), servings: z.number() })
Recipe.exactPartial().parse({}) // ✅
Recipe.exactPartial().parse({ title: undefined }) // ❌
Recipe.partial().parse({ title: undefined }) // ✅TypeScriptのexactOptionalPropertyTypesを有効にしているなら、こちらのほうが型と実行時の挙動が揃います。

exactOptionalPropertyTypesはTypeScriptのコンパイラオプションで、optionalなプロパティとundefinedを入れていいプロパティを区別する設定です。
z.properties()
クラスのインスタンスに対して、複数のプロパティをまとめて検証できます。
これまではz.property()を1つずつ並べる書き方でした。
z.instanceof(URL).check(
z.property("protocol", z.literal("https:" as string)),
z.property("hostname", z.string().regex(z.regexes.domain)),
)v4.5のz.properties()なら、オブジェクトで一度に渡せます。
戻り値が配列なので、.check()にはスプレッドで渡します。
const httpsUrl = z.instanceof(URL).check(
...z.properties({
protocol: z.literal("https:" as string),
hostname: z.string().regex(z.regexes.domain),
}),
)
httpsUrl.parse(new URL("https://example.com")) // ✅
httpsUrl.parse(new URL("http://localhost")) // ❌ expected "https:"as stringが要るのは、URL.protocolの型がstringのためです。
これを外すと、リテラル型と一致しないというエラーになります。
z.creditCard()
クレジットカード番号のフォーマットと、Luhnアルゴリズム(クレジットカード番号の検証アルゴリズム)によるチェックサムを検証します。
z.creditCard().parse("4111111111111111") // ✅
z.creditCard().parse("4111 1111 1111 1111") // ✅ 単一のスペース
z.creditCard().parse("4111-1111-1111-1111") // ✅ 単一のハイフン
z.creditCard().parse("4111 1111 1111 1111") // ❌ 区切りの連続は不可
z.creditCard().parse("4111111111111112") // ❌ チェックサム不一致12桁から19桁を受け付けます。
区切り文字はスペースとハイフンだけで、連続はできません。
カードブランドの判別はしないので、どの発行元の番号でも通ります!
ブランドを絞りたい場合は、これまでどおり自前の判定を足すことになります。
z.creditCard()が見てくれるのは、桁数と区切りとチェックサムまでです。
循環参照を含むデータのparse
自分自身を含むデータを渡すと、v4.4まではエラーになっていました。
const Category = z.object({
name: z.string(),
get subcategories() {
return z.array(Category)
},
})
type Category = z.infer<typeof Category>
const input: Category = { name: "root", subcategories: [] }
input.subcategories.push(input)
const result = Category.parse(input)同じコードを2つのバージョンで走らせた結果です。
zod@4.4.3
RangeError: Maximum call stack size exceeded
zod@4.5.4
result.subcategories[0] === result // true入力にあった循環は、そのまま結果にも残ります!
同じオブジェクトとして扱われるので、===での比較も成立します。
Zod本体では最初から有効になっています。
Zod Miniでは、スキーマを定義する前に登録します。
z.config({ memoizer: z.memoizer() })z.input()とz.output()の関数版
z.inputとz.outputはv4.4まで型だけでしたが、v4.5からは関数としても呼べます。
codecやpipeを含むスキーマから、片側だけを取り出せます。
const isoDate = z.codec(z.iso.datetime(), z.date(), {
decode: (s) => new Date(s),
encode: (d) => d.toISOString(),
})
const Event = z.object({ name: z.string(), at: isoDate })
z.input(Event).parse({ name: "launch", at: "2024-01-01T00:00:00Z" }) // ✅
z.output(Event).parse({ name: "launch", at: new Date() }) // ✅型としての使い方はこれまでどおりです。
z.input()のほうは、codecに付けたcheckを引き継ぎません。
変換した後の値を縛る条件なので、変換前のスキーマには残らないからです。
codecやpipeを含まないスキーマなら、どちらを呼んでも何も変わりません。
z.getDiscriminatedOption()
discriminatedUnionから、特定のdiscriminatorを持つ選択肢を型安全に取り出せます。
const Fruit = z.object({ type: z.literal("fruit"), seeds: z.boolean() })
const Veg = z.object({ type: z.literal("vegetable"), leafy: z.boolean() })
const Produce = z.discriminatedUnion("type", [Fruit, Veg])
z.getDiscriminatedOption(Produce, "fruit") // typeof Fruit
z.getDiscriminatedOption(Produce, "meat") // ❌ TypeScriptのエラーこれまではProduce.optionsから.find()で探して、戻り値の型を手で指定していました。
存在しないdiscriminatorを渡すと、コンパイル時に落ちます。
この関数は公式ドキュメントのAPIページには見当たらず、現時点ではリリースノートだけに載っています。
手元では問題なく動きましたが、ドキュメントに載るまでは仕様が変わる可能性もありそうです。
そのほかの追加はドキュメントで
ここまでのほかにも、細かい追加があります。
z.object()のキーにsymbolを使えるようになったz.nanoid()で長さを指定できるようになった- 対応言語が8言語増えた(ベンガル語・ヒンディー語・ブラジルポルトガル語など)
Defining schemas | Zod
Complete API reference for all Zod schema types, methods, and validation features
zod.dev破壊的変更
リリースノートでは、これらをbug fixesとして扱っています。
どれも本来あるべき挙動に直す修正です。
そのため、これまでの挙動を前提にしていたスキーマでは、判定が変わるかもしれません。
文字列長がUnicodeコードポイント単位に
.min()・.max()・.length()の数え方が変わりました。
UTF-16のコードユニットではなく、Unicodeのコードポイントを数えます。
UTF-16とUnicodeの違いは下記を参考にしてください。
カオス過ぎる Unicode, UTF-8, UTF-16, UTF-32 の違い概要まとめ - Qiita
文字コードについて、Shift-JISもカオスながら、鳴り物入りで出来たUnicodeも色々あるようなので、要点をサクッとまとめ。 とりあえずこれだけ押さえておけばOK Unicode:文字コードの規格の名称。あらゆる国の文字コードを格納できる UCS-4:Unicod...
qiita.comz.string().max(5).parse("😀😀😀😀😀")
z.string().min(5).parse("😀😀😀")max(5) => ERROR too_big: expected string to have <=5 characters
min(5) => OK "😀😀😀"max(5) => OK "😀😀😀😀😀"
min(5) => ERROR too_small: expected string to have >=5 characters挙動が逆転しています!
絵文字1つはUTF-16では2コードユニットなので、"😀😀😀"の.lengthは6でした。
v4.5はこれを3と数えます。
ほかの言語やデータベースに合わせるための変更です。
PostgreSQLやMySQL、GoやPythonは、長さの制限をコードポイントで数えます。
z.toJSONSchema()が出力するmaxLengthも同じです。
変わり方は.min()と.max()で逆です。
.max()は緩む方向にだけ動き、これまで弾いていた入力を通すようになる.min()は厳しくなる方向にだけ動き、これまで通っていた入力を弾くようになる.length()は指定した数によってどちらにも動く
ただし「見た目どおりの1文字」で数えてくれるわけではありません。
家族の絵文字のように複数の絵文字をつないだものは、今も複数文字として数えられます。
"👨👩👧👦".length => 11
コードポイント数 => 7
z.string().max(1) => ERROR too_big見た目は1文字ですが、.max(1)は通りません!
.length()が両方向に動く例です。
.length(1) 4.4.3 => ERROR / 4.5.4 => OK
.length(2) 4.4.3 => OK / 4.5.4 => ERROR絵文字1つはコードユニットでは2、コードポイントでは1と数えるので、通る指定がそのままずれます。

v4.5でもっとも怖いのはこの変更だと思っています。仕様としては正しい方向への修正ですし、他の言語やデータベースに揃うのも納得なのですが、日本語のサービスだとニックネームや自己紹介で絵文字が普通に飛んできます。パッチバージョン感覚で上げると本番で気づくことになりかねません。
z.iso.datetime()
RFC 3339が秒を必須としているため、それに合わせる修正が入りました。
z.iso.datetime().parse("2020-01-01T06:15Z")
zod@4.4.3 => OK "2020-01-01T06:15Z"
zod@4.5.4 => ERROR invalid_format: Invalid ISO datetime秒を省いた形式も受け付けたい場合は、公式が回避策を示しています。
z.union([z.iso.datetime(), z.iso.datetime({ precision: -1 })]){ offset: true }を付けた場合も同じ変更が入ります。
{ local: true }のほうは2020-01-01T06:15を引き続き受け付けます。
タイムゾーンのない日時は、そもそもRFC 3339の対象外だからです。
そのほかの変更はリリースノートで
上の2つ以外にも、挙動の変わった箇所があります。
recordとintersectionの組み合わせがTypeScriptの型どおりに動くようになった.strict()に余分なキーと不正な値を同時に渡すと、issueが2件返るようになった__proto__キーを常に除去するようになったz.ulid()・z.ipv6()・z.httpUrl()・z.emoji()の判定が厳しくなった
心当たりがあれば、リリースノートで詳細を確認してください。
Zod 4.5
Zod 4.5 is now available, with z.compile() ahead-of-time compilation, up to 9x less memory per schema, and a batch of long-awaited features.
zod.devさいごに
v4.5の変更は、自分で呼んで使うものと、上げるだけで効くものに分かれます。
z.compile()は前者です。
サーバーサイドで何度も呼ばれる処理なら、バンドルサイズを気にせず試せます。
クライアントで動かす場合は、7KBの増加と速度を比較して検討する必要がありそうです!
どちらの場合も、{ strict: true }で黙ってフォールバックしていないか確認しておくと確実だと思います!
メモリの削減と失敗時の高速化は後者です。
バージョンを上げるだけで効くので、まずはアップグレードして既存のテストを流してみてください。
%20ahead-of-time%20compilation%2C%20up%20to%209x%20less%20memory%20per%20schema%2C%20and%20a%20batch%20of%20long-awaited%20features.&path=zod.dev%2Fblog%2Fzod-4-5)

&description=z.compile()%20walks%20a%20schema%20once%20and%20produces%20a%20flat%2C%20loop-free%20JavaScript%20validator%20that%20runs%20far%20faster%20than%20the%20standard%20parser.%20Arrays%2C%20unions%2C%20and%20nested%20or%20wide%20objects%20parse%203%20to%207x%20faster%2C%20with%20identical%20output%20and%20identical%20errors.&path=zod.dev%2Fblog%2Fintroducing-z-compile)
%20retained%207.5kb%20of%20heap%20in%20Zod%204.4.%20In%20Zod%204.5%20it%20retains%20784%20bytes.%20One%20change%20to%20where%20a%20schema%27s%20methods%20live%20accounts%20for%20nearly%20all%20of%20it.&path=zod.dev%2Fblog%2Freducing-memory-footprint)

