Loading... # 协作编辑的谎言:第二部分:为什么我们不使用 Yjs # 一、事件概述 ## 1. 事件背景 Moment 团队在实现协作文本编辑器时,对业界流行的协作编辑算法进行了深入评估。在第一部分中,他们发现用户普遍将最流行的协作编辑算法(包括最受欢迎的 Yjs)视为在解决直接编辑冲突时会"静默损坏"他们的文档。在第二部分中,作者进一步论证这些流行算法(尤其是 Yjs)目前也不适合实时协作场景。 ## 2. 核心观点 除非你真正需要完全无主节点的点对点拓扑结构,否则使用 Yjs/CRDT 是过度设计。简单的替代方案(如 prosemirror-collab)可以在几乎每个维度上提供更好的体验。 *** ## 二、核心内容 ## 1. 简单方案演示 作者用一个约 40 行代码的示例证明,实现乐观更新、网络波动时编辑、细粒度编辑溯源等功能,并不需要 CRDT 的复杂性。 ### A. 工作原理 每个文档有一个单一的权威节点,保存: - document:文档内容 - steps:已应用的编辑步骤 - version:当前版本号 客户端提交事务步骤和最后看到的版本号。如果版本不匹配,客户端必须重新获取变更并重新提交变更。 ### B. 权威节点概念 权威节点不意味着"运行在 AWS 的集中式服务器"。你可以将笔记本电脑设置为权威节点,只要与其他人共享即可。因此该协议支持点对点,但它不是"无主节点"的,这正是 CRDT 提供的功能。 ## 2. Yjs 实现的挑战 ### A. 性能问题 Yjs 会在每次击键时完全销毁并重新创建整个文档。这不是意外,而是设计决策。这导致: - 性能下降,因为每次击键都需要重新创建所有 NodeView、装饰和 DOM 元素 - 破坏依赖位置映射的插件(如评论和协作存在指示器) - 撤销、光标位置和选择管理变得极其奇怪 - 文档中所有小组件的状态持续被完全擦除 ### B. 延迟性能目标 作者的目标是编辑器在 60 fps 下运行,这意味着最大约 16ms 的时间来完成所有工作。使用 ProseMirror 时: - 事务应用非常快 - 服务器将事务批处理为不超过 20 个步骤的块 - 冲突解决从不在主线程上发生 - DOM 对象的更新是增量的 而 Yjs 每次协作击键都从零开始替换整个文档。 ### C. 文档架构冲突 ProseMirror 文档架构用于定义文档结构的规则(如 blockquote 节点不能是 code_block 节点的子节点)。在集中式设置中,服务器可以拒绝无效的事务。 Yjs 设计用于真正的无主节点 P2P 拓扑,其默认设置更加危险。当 schema.node() 因架构无效而抛出异常时,节点似乎被永久删除,该删除传播到所有对等节点。 ### D. 权限控制更痛苦 真实世界的文档编辑器通常会提供各种权限。使用 ProseMirror 时,查看事务并检查它如何改变文档即可实现权限控制。 在 Yjs 中,由于它将事务映射到 XML 更新,你基本上必须预测当它物化为事务时的净效果,并根据该预测接受或拒绝。这要困难得多。 ### E. 可用性并不更好 关于 Yjs 的一个常见论点是它让服务器宕机时更容易保持运行。但作者认为这对大多数现实应用来说并不正确。 现代文本编辑器几乎都做很多非存储文本的事情: - 将内容存储在媒体服务器中 - 检查权限 - 存在服务可能是独立服务 - 持久化(如文档存储在 S3,操作存储在 K/V 存储中) 如果这些服务中的任何一个(尤其是权限)宕机,你可能需要停止提供流量。 ### F. 墓碑机制的数据丢失或内存消耗 协作编辑的"简单"解决方案将所有步骤存储在持久化存储中。Yjs 被设计用于真正的无主节点 P2P 拓扑,通常无法轻松忘记步骤。如果一个项目被删除,它必须保留一个"墓碑"——一个记录项目已被删除的标记。 通用解决方案是垃圾回收墓碑。但你只能在所有对等节点都删除了项目时安全地这样做——而你不能确定这一点,因为你无法区分断开连接和慢速客户端。因此,你可以让墓碑保留更长时间(占用内存),或者在一些任意时间后忘记它们,这将会丢失数据。 ### G. 调试困难得多 作者已经实现了几乎所有方式的协作文本编辑器:OT、CRDT、使用 prosemirror-collab、使用 prosemirror-collab-commit、使用锁定。 在"简单"解决方案中,你有许多工具可以帮助找出这些错误: - 为每个请求附加幂等性键 - 调度每个写请求两次以清除竞态条件 - 积极测试服务器的竞态 但如果使用 CRDT,所有这些问题要难 100 倍,而且这些缓解措施对你都不可用。根据定义,状态只保证收敛。你怎么能知道某些东西是暂时分歧,还是根本不正确?答案是不能。 ```mermaid graph TD A[协作编辑方案选择] --> B{需要真正的无主P2P?} B -->|是| C[使用Yjs/CRDT] B -->|否| D[使用prosemirror-collab] D --> E[权威节点模式] E --> F[简单的版本控制] F --> G[增量更新] G --> H[更好的性能] H --> I[更易调试] C --> J[墓碑机制] J --> K[内存消耗高] K --> L[可能数据丢失] C --> M[整文档重建] M --> N[性能问题] N --> O[权限控制困难] ```  ## 3. 核心结论 作者希望说服读者,除非你真正需要完全无主节点的点对点拓扑,否则最好使用"简单"解决方案。 对于库作者的最终建议:**设计库时,你必须从你想要启用的最终用户体验开始,而不是从算法开始。** *** ## 三、技术细节 ## 1. Yjs 的架构问题 Yjs 无法直接表示富文本编辑(这是一个真正开放的研究问题)。相反,Yjs 使用其 XML 设施表示 ProseMirror 文档。这意味着它们不能直接使用 ProseMirror Transaction 对象,写入必须将 Transaction 转换为 Yjs XML 更新;客户端同样接收更新并需要以某种方式将 Yjs XML 更新转换回 Transaction 并将其应用到 ProseMirror 文档。 所有这些都有代价。即使它们很便宜,Yjs 仍然坚持在每次协作击键时替换整个文档。 ```mermaid sequenceDiagram participant Client as 客户端 participant PM as ProseMirror participant Yjs as Yjs XML participant Server as 服务器 Client->>PM: 用户编辑 PM->>PM: 创建 Transaction PM->>Yjs: 转换为 XML Yjs->>Server: 发送更新 Server->>Yjs: 接收其他用户的更新 Yjs->>PM: 转换回 Transaction PM->>Client: 重建整个文档 ```  ## 2. 性能目标对比 | 指标 | ProseMirror 方案 | Yjs 方案 | |-------|----------------|----------| | 事务应用 | 极快,支持重定基线 | 慢,每次重建整个文档 | | 批处理 | 服务器可批处理 20 个步骤 | 不支持增量批处理 | | 主线程影响 | 冲突解决可在工作线程中 | 无法在工作线程中运行 | | DOM 更新 | 增量更新 | 每次击键重建整个 DOM | *** ## 四、影响分析 ## 1. 行业影响 这篇文章挑战了行业对 CRDT 的盲目推崇,提供了基于实际工程经验的独立观点。它提醒开发者在技术选型时应该: - 从实际需求出发 - 考虑工程复杂度 - 评估性能影响 - 不要被"流行"方案蒙蔽 ## 2. 对开发者的启示 ### A. 避免过度设计 如果你的应用不需要真正的无主节点 P2P 架构,CRDT 可能是过度设计。简单方案(40 行代码)可以满足大多数需求。 ### B. 重视性能 60 fps 是编辑器的健康标准,任何影响这一目标的设计都需要认真评估。Yjs 的整文档重建策略与这一目标直接冲突。 ### C. 考虑可维护性 调试 CRDT 系统比调试简单的权威节点方案要困难得多。选择技术时要考虑长期的维护成本。 ### D. 从用户体验出发 技术选型应该以最终用户体验为起点,而不是从算法的学术特性出发。用户的真实需求是:协作、容错、流畅的编辑体验。 *** ## 五、各方反应 ## 1. 社区讨论 文章引用了 Yjs 作者和 ProseMirror 作者在 6 年前关于 replace-everything 策略的讨论,显示了社区对这一问题的长期争议。 ## 2. 改进进展 Yjs 维护者正在努力改进问题(如 y-prosemirror PR #216),尝试使更新更加细粒度。但作者表示,目前的状态仍然不理想。 *** ## 六、总结 ### A. 做得好的地方 - 提供了简洁的替代方案实现 - 从多个维度(性能、架构、可维护性)分析问题 - 基于实际工程经验而非理论假设 ### B. 核心要点 1. **除非你真正需要完全无主节点的 P2P 架构,否则 CRDT 是过度设计** 2. **简单的权威节点方案可以满足大多数协作编辑需求** 3. **Yjs 存在多个严重的工程问题**:性能、架构兼容性、权限控制、调试困难 4. **技术选型应该从用户体验出发,而不是从算法特性出发** ### C. 适用场景 - **推荐 prosemirror-collab 的场景**:大多数协作编辑应用,有集中式服务器或权威节点 - **推荐 Yjs/CRDT 的场景**:真正的无主节点 P2P 协作,如完全去中心化的编辑应用 *** ## 参考资料 1. [Lies I was Told About Collaborative Editing, Part 2: Why we don't use Yjs](https://www.moment.dev/blog/lies-i-was-told-pt-2) 2. [Yjs GitHub](https://github.com/yjs/yjs) 3. [ProseMirror collab](https://github.com/ProseMirror/prosemirror-collab) 最后修改:2026 年 03 月 17 日 © 允许规范转载 赞 如果觉得我的文章对你有用,请随意赞赏