Penpot 作为开源 Web 设计工具,以 SVG 为核心格式,支持多用户实时协作编辑界面原型。这种场景下,用户同时操作形状层、调整布局或切换原型状态,传统锁机制会导致延迟,而 Operational Transformation (OT) 或 Conflict-free Replicated Data Types (CRDT) 可实现无冲突同步。CRDT 更适合分布式浏览器环境,因其操作可交换性强、无需中心协调服务器,推荐用于 Penpot-like SVG 编辑器。
OT 与 CRDT 的对比与选择
OT 通过服务器转换并发操作序列,确保最终一致,如 Google Docs 早期采用,但对 SVG 复杂结构(如嵌套路径、渐变)变换规则繁杂,易引入 bug。CRDT 则设计数据类型本身支持合并,例如 Yjs 库的 CRDT 文档模型,每个形状操作带唯一 ID 和因果向量,最终状态自动收敛。“Yjs 采用 CRDT 方案,每个操作有唯一标识,最终结果一致。” 相比 OT,CRDT 在 P2P 或弱网下更鲁棒,空间复杂度 O (n) 但浏览器 IndexedDB 可优化。
在 Penpot 中,SVG 元素树(groups/layers)可映射为 CRDT Map/Array:根为文档,子为形状层。用户 A 添加矩形、B 同时拖拽,CRDT 合并为并存状态,避免覆盖。
CRDT 在 SVG 形状编辑中的核心实现
SVG 编辑的核心是操作原子化:insert/delete/update 形状属性(position, fill, stroke)。用 Yjs 的 Y.Map 表示形状库,Y.Array 管理层序,每个形状 ID 为 UUID,属性如 {x: CRDT 数,y: CRDT 数,width: CRDT 数}。
-
形状 CRDT 定义:
- 位置:用 Position CRDT,支持并发 insert/delete。
- 变换:Matrix 分解为 translate/scale/rotate,各用独立 CRDT。
- 路径:序列化 d 属性为 Y.Text CRDT,命令如 M/L/C 独立合并。
-
层级与原型集成:
- 层:Y.Array<Y.Map>,支持 splice/retain 并发。
- 原型:状态机 CRDT,variant 字段记录当前帧,交互 overlays 用 Y.Map 覆盖。
-
同步架构:
- 前端:Y.Doc + WebsocketProvider(ws://localhost:1234)。
- 后端:Y-Sweet 或 Redis Pub/Sub 广播 delta。
- 离线:IndexedDB 持久 Y.Doc,reconnect 时 merge。
可落地工程参数与清单
实现冲突 - free 实时 SVG 编辑,关键参数如下,确保 < 100ms 延迟、99.9% 收敛:
1. 同步参数
| 参数 | 值 | 说明 |
|---|---|---|
| heartbeat | 30s | WebSocket 保活,超时 3 心跳断开重连 |
| debounce | 16ms | 操作批次(RAF),减少广播 |
| maxDeltaSize | 1KB | 单包限,超长拆分 |
| awarenessInterval | 100ms | 光标 / 选区广播,避免抖动 |
清单:
- 初始化:
const ydoc = new Y.Doc(); const provider = new WebsocketProvider('ws://...', 'penpot-room', ydoc); - 形状绑定:
const shapes = ydoc.getMap('shapes'); shapes.observe(() => renderSVG()); - 光标:
yAwareness.setLocalState({user: 'A', cursor: {x,y}});
2. 冲突解决阈值
- 位置冲突:>5px 偏移视为独立,merge 用平均或 last-writer-wins(LWW)寄存器。
- 层序:CRDT Array 天然支持并发 splice。
- 原型状态:用 Y.Map 'prototype' {current: 'frame1', overrides: {shape1: {fill: 'red'}} }。
回滚策略:快照每 5min,diff 回放 < 10s。
3. 性能监控点
- CPU:CRDT merge <5ms/op(Chrome DevTools)。
- 内存:Y.Doc <50MB / 文档(gzip delta)。
- 带宽:用户 N=10,峰值 < 1KB/s/user。
4. 代码 / 组件导出集成
- 钩子:shapes 变化时,exportSVG () 生成,代码 inspect 用 getComputedStyle。
- 组件:Y.Map 'components' 序列化 React/Vue props,确保导出时 CRDT 收敛。
示例伪码:
// 新形状
const id = uuid();
shapes.set(id, ydoc => ({
type: 'rect',
x: new Y.RelativeNumber(0), // CRDT pos
y: 0,
observers: [] // 绑定渲染
}));
// 拖拽更新
function drag(shapeId, dx, dy) {
const shape = shapes.get(shapeId);
shape.x.addNumber(dx); // 原子增量
}
部署与测试清单
- 库:Yjs@beta, y-websocket, ProseMirror(SVG 扩展)。
- 测试:并发 100op/s,JMeter 模拟 10 用户,验证收敛率 100%。
- 监控:Prometheus + Grafana,alert delta backlog>1s。
- 回滚:toggle OT fallback,若 CRDT merge 失败。
此方案在 Penpot 架构下,直接复用 SVG 渲染栈,扩展 multiplayer。实际部署,结合 WebRTC P2P 降中心负载。
资料来源:
- Penpot GitHub: https://github.com/penpot/penpot
- HN 讨论: https://news.ycombinator.com/item?id=41986992
- Yjs 文档与 CRDT 实践。
(正文字数:1256)