Why does tldraw use tlschema instead of zod?
This post might have stale content, as HEAD is 337 commits ahead.
详细解释 tldraw 为什么实现了自定义的 tlschema,为什么不使用 zod,我是否可以使用 zod 替代其内部实现,详细解释
关键的优化逻辑:
// 1. 快速路径:引用相等 = 没有改变
if (Object.is(knownGoodValue, newValue)) {
return knownGoodValue // ⚡ 零成本返回!
}
// 2. 每个数组元素检查
for (let i = 0; i < arr.length; i++) {
// 3. 元素级别的快速检查
if (Object.is(knownGoodValue[i], item)) {
continue // ⚡ 跳过已验证的元素!
}
// 4. 只验证改变的元素
const checkedItem = itemValidator.validateUsingKnownGoodVersion!(
knownGoodValue[i],
item
)
// 5. 跟踪整体是否改变
if (!Object.is(checkedItem, knownGoodValue[i])) {
isDifferent = true
}
}
// 6. 返回原对象或新对象
return isDifferent ? (newValue as T[]) : knownGoodValue
这意味着:
- ✅ 引用相等检查:O(1) 复杂度
- ✅ 元素级判断:只验证改变的元素
- ✅ 对象复用:未改变则返回原引用(支持 React 优化)
###2️⃣ 使用场景:为什么 Zod 不行
考虑 tldraw 中的实时更新场景:
// RecordType.ts 中的使用
validate(record: unknown, recordBefore?: R): R {
// 如果有前一个版本,使用优化的验证路径
if (recordBefore && this.validator.validateUsingKnownGoodVersion) {
return this.validator.validateUsingKnownGoodVersion(recordBefore, record)
}
return this.validator.validate(record)
}
// 用户每次操作都会调用这个
store.put(shape, shapeOldVersion) // ⭐ 传入前一版本
####Zod 的问题
// Zod 的方式 ❌
const shapeSchema = z.object({
id: z.string(),
x: z.number(),
y: z.number(),
rotation: z.number(),
// ... 20 个其他属性
props: z.object({
// ... 嵌套结构
})
})
// 每次调用都要完整验证所有属性,包括那些没改的
const validated = shapeSchema.parse(newShape) // 总是全量验证!
###3️⃣ 额外原因:迁移系统
tldraw 的数据需要持久化和版本演进:
// tlschema 为每个数据类型都定义了迁移序列
export const imageShapeMigrations = defineMigrations({
currentVersion: Versions.AddMinMaxScale,
firstVersion: Versions.InitialAddAssetBinding,
migrators: {
[Versions.AddAssetBinding]: {
up: (record) => ({ ...record, assetId: null }),
down: ({ assetId, ...record }) => record,
},
[Versions.AddMinMaxScale]: {
up: (record) => ({ ...record, minScale: 0, maxScale: 1 }),
down: ({ minScale, maxScale, ...record }) => record,
},
},
})
tldraw 的迁移系统:
- 可以向前和向后迁移(up/down)
- 支持复杂的跨版本转换
- 与验证系统深度集成
Zod 的方式:
- 没有内置迁移系统
- 无法优雅地处理数据版本演进
- 需要手动管理迁移逻辑
###4️⃣ 为什么不能用 Zod 替代
| 特性 | tldraw (@tldraw/validate) | Zod |
|---|---|---|
| validateUsingKnownGoodVersion | ✅ 有 | ❌ 无 |
| 性能优化 | ✅ 结构共享,跳过验证 | ❌ 总是全量验证 |
| 迁移系统 | ✅ 内置完整迁移支持 | ❌ 无 |
| 向后兼容 | ✅ up/down 迁移 | ❌ 单向 |
| 错误路径 | ✅ 详细的嵌套路径 | ✅ 有(但不同格式) |
| TypeScript 类型推断 | ✅ 完整 | ✅ 完整 |
直观比较:
// ❌ 用 Zod 替代 tldraw 验证会导致:
// 1. 失去性能优化(每次都全量验证)
// 2. 失去迁移能力(无法处理版本升级)
// 3. 无法复用之前验证的对象引用
// 4. 在高频更新场景下性能严重下降
// ✅ tldraw 的方案才能提供:
const validator = T.object({...})
// 第一次验证
const shape = validator.validate(rawShape)
// 后续更新只验证改变的部分!
const updated = validator.validateUsingKnownGoodVersion(shape, newRawShape)
###5️⃣ 能否使用 Zod 替代?
直接替代:不能
但是,如果你不需要:
- 高频数据验证的性能优化
- 复杂的数据迁移系统
- 向后兼容的数据格式演进
那么可以在你自己的应用中使用 Zod:
// ✅ 可以在应用代码中使用 Zod
import { z } from 'zod'
const shapeSchema = z.object({
id: z.string(),
type: z.literal('geo'),
x: z.number(),
y: z.number(),
})
// 但核心的 tldraw 库内部会继续用 @tldraw/validate
import { T } from '@tldraw/validate'
何时用 Zod:
- API 请求验证
- 配置文件解析
- 一般的表单验证
- 不需要频繁验证同一对象的场景
何时用 @tldraw/validate:
- tldraw 内部的形状和记录验证
- 实时协作系统
- 需要性能优化的高频验证
- 需要完整迁移系统的数据模型
###6️⃣ 为什么 tldraw 模板中用了 Zod?
你看到的模板(agent、chat 等)使用 Zod 是因为:
// templates/agent 中的 Zod 使用
// 这些是 AI agent 的 action schemas,不是 tldraw 核心数据
// 这里用 Zod 完全没问题,因为:
// 1. 不需要高频更新
// 2. 不需要向后兼容迁移
// 3. 只是验证一次 AI 的响应
##总结
| 问题 | 答案 |
|---|---|
| 为什么实现自定义 tlschema? | 需要 validateUsingKnownGoodVersion 性能优化 + 完整迁移系统 |
| 为什么不用 Zod? | Zod 缺少性能优化机制,每次都全量验证;缺少迁移系统 |
| 能用 Zod 替代吗? | 不能替代核心库,但可在应用代码中并存使用 |
| 关键差异 | 结构共享、跳过验证、元素级增量验证、向前向后迁移 |
tldraw 的设计是权衡的典范:为了支持高性能的实时协作编辑和完整的数据演进,选择了构建自己的验证系统,而不是依赖通用的 Zod。
END OF POST