ZodTypeScriptSchema ValidationFrontend EngineeringMigration Guide

Zod 4 效能與遷移指南:官方 Benchmark 與升級重點

解析 Zod 4 官方 benchmark 與 core bundle benchmark,整理 Zod Mini API 差異與 Zod 3 升級 checklist,避免誤讀效能數字。

· 6 分鐘閱讀

看懂 Zod 4 的官方數據應該怎麼解讀,並用一份可執行的清單把 Zod 3 專案穩定升級。


前言

Zod 4 發布後,大家最常引用的關鍵字是「14x 更快」和「體積變小」。但在實務上,這兩句話如果沒有上下文,很容易被誤用成「所有解析都 14x」或「整個套件永遠小一半」,最後導致技術決策變成口號。

這篇文章的重點不是幫 Zod 4 做行銷,而是把官方資料拆回工程語言:哪些數字來自哪一種 benchmark、哪些只代表 core bundle 的最小案例、哪些 migration 變更真的會影響既有專案。

如果你正在考慮升級,或正在 review 團隊的升級 PR,本文可當作一份「快速核對清單 + 範例模板」。


Zod 4 的效能數字要怎麼看:benchmark 先分場景

官方 release notes 內的效能數字分別來自不同測試場景,不能直接合併成一句「全面 14x」。你可以先抓三個最重要的基準:

  1. z.string().parse:約 14.71x(相對 Zod 3)
  2. z.array() parsing:約 7.43x
  3. z.object().safeParse:約 6.5x

這三個數字本身都成立,但它們描述的是不同資料型態與不同 API(parse vs safeParse)下的結果。

實務上比較安全的寫法是:

  • 字串解析在官方基準中提升幅度最大(約 14.71x)。
  • 陣列與物件場景也有顯著改善(約 7.43x、6.5x)。
  • 真實專案效能仍取決於 schema 複雜度、資料形狀與錯誤分支比例。

下面是你可以在本地做的「最小可重現測試」範例,避免只看網路數字:

npm i zod@4 tinybench tsx -D
// scripts/bench-zod-parse.ts
import { Bench } from "tinybench";
import * as z from "zod";

const UserSchema = z.object({
  id: z.string(),
  email: z.email(),
  roles: z.array(z.string()),
  profile: z.object({
    displayName: z.string().min(2),
    age: z.number().int().nonnegative(),
  }),
});

const goodPayload = {
  id: "u_123",
  email: "dev@example.com",
  roles: ["admin", "editor"],
  profile: { displayName: "Owl", age: 28 },
};

const bench = new Bench({ time: 1000 });

bench
  .add("parse", () => {
    UserSchema.parse(goodPayload);
  })
  .add("safeParse", () => {
    UserSchema.safeParse(goodPayload);
  });

await bench.warmup();
await bench.run();

console.table(
  bench.tasks.map((task) => ({
    name: task.name,
    hz: task.result?.hz,
    meanMs: task.result ? task.result.mean * 1000 : null,
  }))
);
npx tsx scripts/bench-zod-parse.ts

這樣你可以用自己的 schema 做 A/B,比只引用單一官方圖表更接近決策需求。


體積變小不是萬靈丹:先理解「core bundle benchmark」

Zod 4 常被引用的另一組數字是:

  • Zod 3:12.47kb(gzip)
  • Zod 4:5.36kb(gzip)
  • Zod Mini:1.88kb(gzip)

這組數據來自官方用 z.boolean() 這種極簡腳本搭配 rollup 的 core bundle benchmark。它的價值是:幫你比較「最基本情境下的核心成本」,而不是代表所有專案打包後都會固定落在這三個數字。

換句話說,你可以把它理解成「起跑線比較」,不是「最終成績保證」。

什麼情況該考慮 Zod Mini

官方文件對 Zod Mini 的定位很清楚:它功能等價,但 API 偏 functional,目標是更容易 tree-shake。這對前端 bundle 敏感場景有價值,但 DX(可讀性、可發現性)通常不如標準 Zod 直覺。

最常見的決策準則:

  1. 先用標準 zod 開發。
  2. 實測發現 bundle 預算壓力確實存在(例如 web vitals、低速網路目標市場)。
  3. 再評估把高流量前端入口改為 zod/mini

Zod 與 Zod Mini API 差異:重點在 .check()

Zod Mini 不是「小一點的 Zod 方法鏈」,而是把很多驗證條件改成函式傳入 .check()

// regular zod
import * as z from "zod";

const LoginSchema = z.object({
  email: z.email(),
  password: z.string().min(12).max(128).trim(),
});
// zod mini
import * as z from "zod/mini";

const LoginSchema = z.object({
  email: z.email(),
  password: z
    .string()
    .check(
      z.minLength(12), // 最短 12 字元
      z.maxLength(128), // 最長 128 字元
      z.trim() // 解析前先去除首尾空白
    ),
});

Zod Mini 仍保留相同的 parsing methods(parsesafeParseparseAsyncsafeParseAsync),所以你可以沿用既有資料流;真正要適應的是 schema 定義風格。

這也是為什麼在大型團隊裡,常見做法不是「全案一口氣改 Mini」,而是只在對 bundle 最敏感的頁面或 package 試點。


遷移重點:這些才是 Zod 3 -> 4 真正常踩雷的地方

舊文最常見的誤解是把 z.infer 當成被淘汰語法。實際上,Zod 4 仍可正常使用 z.infer。升級時更該優先檢查的是下列 breaking/deprecation:

  1. 統一 error 參數(過去 errorMap 改名為 error)。
  2. z.record() 單參數形式移除,需明確給 key/value schema。
  3. ZodObject.merge() deprecated,改用 extend()
  4. .nonempty() 在 array 的型別推斷改變(行為接近 .min(1),不再推成 tuple-like)。
  5. 字串格式 API(如 email)移到 top-level z namespace,較利於 tree-shaking。

遷移對照範例

import * as z from "zod";

// 1) z.record: 單參數不再支援
// Zod 3: z.record(z.string())
const HeadersSchema = z.record(z.string(), z.string());

// 2) .merge() -> .extend()
const BaseUser = z.object({ id: z.string() });
const WithProfile = z.object({ displayName: z.string() });
const UserSchema = BaseUser.extend(WithProfile.shape);

// 3) string format top-level API
const ContactSchema = z.object({
  email: z.email(),
  website: z.url(),
});

// 4) z.infer 仍可用
type User = z.infer<typeof UserSchema>;

建議的升級流程(團隊版)

# 1. 升級套件
pnpm add zod@^4

# 2. 先找明確會動到的 API
rg -n "errorMap|\.merge\(|z\.record\([^,)]*\)|\.nonempty\(" src

# 3. 跑型別檢查與測試
pnpm tsc --noEmit
pnpm test

# 4. 再跑實際資料流測試(API contract / form submission)

如果你們是 monorepo,建議先挑一個 schema 依賴最重、但業務風險中等的 package 做 pilot,讓 migration pattern 成熟後再推廣。


什麼情境值得升級、什麼情境可以先觀望

優先升級的情境

  • API gateway / BFF 有大量 runtime validation。
  • 前端表單量大,且 parse/safeParse 是 request path 熱點。
  • 你正好要做 schema 層重構(一次處理技術債成本最低)。

可以先觀望的情境

  • 目前 Zod 3 非瓶頸,且團隊短期沒有 refactor capacity。
  • 你們對 bundle size 並不敏感(例如純後端、內網工具)。
  • 專案正在大改版上線窗口,無法承受驗證層變更風險。

工程決策不一定要「第一天就上最新」,但建議至少做一次 PoC,用你自己的數據決定是否升級。


常見問題 / 注意事項

z.infer 在 Zod 4 還能用嗎?

可以。z.infer 仍是官方文件常見用法,並非淘汰 API。

Zod Mini 會讓所有專案都更快嗎?

不一定。它主要優勢是更容易 tree-shake 的 bundle 成本,不等於 runtime validation 一定更快。

我可以直接把所有 .email() 換成 z.email() 嗎?

建議逐步替換並配合型別測試。Zod 4 的 top-level string format API 是推薦方向,但大型專案應避免一次大改。

遷移時最容易漏掉什麼?

通常是 z.record() 單參數用法與 .merge(),因為舊程式可讀性高、不容易第一眼看出是升級風險點。


總結

Zod 4 的價值可以濃縮成三句話:

  1. 官方 benchmark 顯示明顯效能提升,但要按場景解讀,不要泛化成單一倍率。
  2. bundle size 改善成立,但請用「core bundle benchmark」理解它,而不是當成所有專案最終體積。
  3. 遷移重點不在 z.infer,而在 error 參數統一、z.record().merge().nonempty() 與 string format API 調整。

如果你要把這次升級做得穩,最有效的做法是:先跑小範圍 PoC、建立遷移對照規則,再批次推進到整個專案。這比一次全面替換更可控,也更容易讓團隊接受。