技术解读

以下内容由 GitRead 中文社区 编辑团队基于项目 README 与官方资料整理, 结合中文开发者的常见使用场景补充技术选型建议。具体以项目官方文档为准。

Triplit 深度解析:把数据库塞进客户端,是野心还是解药?

项目背景与痛点

前端数据层的复杂度,几乎全部来自一个事实:权威数据住在远端,而 UI 需要它立刻可用。于是我们发明了一整套补偿机制——React Query / SWR 做缓存、手写乐观更新、自己维护 invalidation key、用 WebSocket 接收推送再手动 patch 缓存。业务代码里塞满了"如果请求失败就把本地状态回滚"的胶水,而这些胶水往往比业务逻辑本身还多。

Firebase、Supabase、Convex 这类 BaaS 把实时订阅做掉了,但代价是把数据主权和部署形态交给厂商。离线支持要么缺失,要么是"缓存一份 JSON"级别的敷衍——你没法在客户端对缓存做带 where 和关系 join 的查询。

Triplit 的切入点很直接:不要把同步当作网络层的问题,要把它当作数据库的问题。它在客户端跑一个真数据库(IndexedDB / SQLite / 内存),在服务端跑另一个(SQLite / Cloudflare Durable Objects),中间用 WebSocket 做双向增量同步。上层只面对一套 schema 和一套查询 API——查询即订阅,结果随数据自动重算。

核心技术揭秘

理解 Triplit 的关键,在于它的存储模型:它不是一个行式/文档式存储,而是属性级的实体-属性-值(EAV)三元组。每个变更的单位是「某实体的某个字段」。

这个选择几乎决定了后续所有能力。字段级增量意味着同步包可以极小;意味着读权限可以精确到列;意味着两个客户端同时改同一实体的不同字段时不会互相覆盖。代价是查询执行需要更多重组工作,但换来的是同步层和权限层的巨大简化——这是典型的「把复杂度挪到本地」的取舍。

同步的闭环大致如下:

graph TD A[应用组件触发 mutation] --> B[本地数据库(IndexedDB / SQLite)] B --> C[乐观更新:立即重算订阅查询] B --> D[变更队列] D --> E[WebSocket 上行同步] E --> F{"服务端校验:schema 与权限"} F -->|"通过"| G[持久化存储(SQLite / Durable Object)] F -->|"拒绝"| H[下发回滚指令] H --> I[撤销乐观更新并通知订阅者] G --> J[匹配活跃查询订阅] J --> K[增量广播给相关客户端] K --> L[客户端应用远端变更] L --> C

几个值得拆开说的设计点:

查询订阅的匹配索引。 服务端并不广播全量变更,而是维护一张「活跃订阅表」:每条订阅记录其 collection 与过滤条件,写入时按 collection 分桶再做谓词匹配,只把命中的变更推给对应连接。这就是为什么 Triplit 能在服务端支撑大量细粒度订阅——本质是一个内存中的发布/订阅路由表,而不是简单的广播房间。

Durable Objects 版本是个聪明的架构妥协。 Cloudflare 的 DO 天然保证单实例、单写者、串行执行,这恰好解决了边缘部署中最麻烦的协调问题:同一个 DO 内部的订阅与写入天然一致,不需要额外锁。代价是放弃了跨 DO 事务,扩展性靠「选对分区键」而非全局一致。这不是缺陷,是明确的取舍。

乐观更新 + 服务端裁决的收敛模型。 客户端先写本地、先渲染,变更进入队列等待服务端确认;被拒绝则回滚并触发订阅重算。冲突策略是务实的 last-write-wins 风格,而非 CRDT——这决定了它做不了文本协同编辑这类语义合并场景,需要外挂 Yjs 之类的方案。

schema 单一来源。 TypeScript 定义的 schema 同时驱动客户端类型推导、服务端校验和数据库迁移,查询结果的类型是从 schema 推导出来的而非手写泛型。这一点对开发体验的加成,比任何性能数字都重要。

功能亮点与差异

第一,可自托管。 这是它与 Firebase/Supabase 最大的分野:一个 Node 进程 + 一个 SQLite 文件就能起服务端,不必把数据交给第三方。对企业内网、合规敏感场景,这条几乎是一票否决项。

第二,存储可插拔但 API 一致。 IndexedDB、SQLite、Durable Object 背后是不同的运行时(浏览器、React Native/Expo、Workers),但上层查询语义保持一致。这需要相当克制的抽象层设计——把 SQL 方言差异封在适配器里,而不是泄漏到查询 API。

第三,关系查询在客户端完成。 支持带关联的嵌套查询与订阅,本地 join 而非多次往返。这让"列表 + 详情"类 UI 的实现从 N 次请求变成一次订阅。

第四,绑定层极薄。 React / Solid / Vue / Svelte 各自一个包,做的是把订阅接到各框架的响应式原语上,而不是重造一套状态管理。

应用场景与落地建议

适合:离线优先的协作工具(任务板、笔记、CRM)、弱网/移动端应用、边缘低延迟场景、以及需要数据自托管的企业内部系统。

不适合:需要跨实体强事务的记账类业务;海量数据的分析型查询;以及需要语义合并的富文本协同(LWW 会丢编辑)。

生产环境几条硬建议:

  • 同步范围要克制。 客户端只能查询已同步下来的数据,范围设计过宽会导致首屏拉取爆炸。按用户/租户/项目做分区,而不是"整库同步"。
  • 权限规则必须在服务端执行,别信客户端。 客户端校验只是体验优化。
  • schema 迁移是真实痛点。 客户端存在多版本共存(用户不刷新页面),变更需要向前兼容,破坏性改动要分多步上线。
  • 监控订阅数、下行字节和回滚率。 回滚率异常升高通常意味着规则写错或冲突策略不适配业务。
  • 接受它是一个仍在快速演进的项目。 API 稳定性需要自行评估,重要路径建议做一层薄封装隔离。

综合评价

优点:架构方向正确——把同步下沉到数据层,而不是让每个业务页面自己解决;EAV 模型带来的字段级同步与权限是真正的差异化能力;自托管 + 可插拔存储让它能进企业;类型链路从 schema 贯通到查询结果,工程体验在线。

缺点:LWW 冲突策略决定了它够不到协同编辑的深水区;客户端数据边界要求开发者提前想清楚同步范围,学习曲线不在 API 而在"分布式数据建模"这个思维转换上;生态与社区规模尚小,遇到边缘 case 基本只能读源码;0.x 阶段的 API 演进会带来迁移成本。

一句话总结:它不是又一个 Firebase 克隆,而是把"本地优先"从理念做成了可用的工程实现。 如果你的痛点是"实时数据 + 离线可用 + 不想被云绑定",它值得认真评估;如果你要的是强一致事务或文本级协同,它现在还不是答案。

关于

GitHub 原始描述(英文)

A full-stack, syncing database that runs on both server and client. Pluggable storage (indexeddb, sqlite, durable objects), syncs over websockets, and works with your favorite framework (React, Solid, Vue, Svelte).

描述取自 GitHub 官方 API,保持仓库作者书写的原文。

简要分析

热度分
3339
价值分
1901
活跃状态
活跃

语言占比

CSS
Dockerfile
HTML
JavaScript
MDX
Shell
Svelte
TypeScript
Vue

Release