CRDTs for Real-Time Collaboration
CRDTs (Conflict-free Replicated Data Types) enable real-time collaboration without a central authority resolving conflicts. Essential for local-first and multiplayer apps.
CRDT vs Operational Transform
| Aspect | OT | CRDT |
|---|---|---|
| Architecture | Requires central server | Works P2P |
| Offline | Complex | Native support |
| Complexity | Server-side transforms | Client-side, simpler |
| Latency | Round-trip to server | Immediate local |
Figma’s approach: CRDT-inspired, but uses a central server for simplicity. Pure CRDTs work fully P2P.
Top Libraries (2025)
Loro (Recommended)
High-performance Rust CRDT library with JS/Swift bindings.
- Performance focus: Optimized memory, CPU, loading speed
- Rich types: Text, map, list, movable tree
- Figma-style canvas: Lists/trees with undo/redo
- Time travel: Built-in version history
Uses algorithms from Diamond-types (Eg-walker), Automerge (columnar encoding), and Yjs (merge operations).
import { Loro } from "loro-crdt";
const doc = new Loro();
const list = doc.getList("objects");
list.insert(0, { type: "rect", x: 100, y: 100 });
// Sync with another peer
const updates = doc.exportFrom(lastVersion);
otherDoc.import(updates);
Yjs
Mature, widely used. Great for text collaboration.
- Modular architecture
- Many editor integrations (Quill, ProseMirror, Monaco)
- Requires custom UI development
Automerge
JSON document model, multi-language support.
- Familiar API
- Historical performance issues (improving)
- Good for document-centric apps
Canvas Architecture Pattern
┌─────────────────────────────────────┐
│ UI Layer │
│ (Fabric.js, Konva, custom) │
└──────────────┬──────────────────────┘
│ events
┌──────────────▼──────────────────────┐
│ CRDT Layer (Loro) │
│ • Object list │
│ • Property maps │
│ • Z-order tree │
└──────────────┬──────────────────────┘
│ sync
┌──────────────▼──────────────────────┐
│ Transport │
│ • WebSocket (server relay) │
│ • WebRTC (P2P) │
│ • IndexedDB (offline) │
└─────────────────────────────────────┘
Key Patterns
1. Separate Cursors from Document
Cursor positions change constantly. Don’t sync them through the CRDT — use a separate ephemeral channel (WebSocket broadcast).
2. Operation Granularity
Too fine (every keystroke) = network spam. Too coarse (save on blur) = merge conflicts.
Batch operations on idle/debounce.
3. Undo/Redo
CRDTs make this complex — undoing your change shouldn’t undo someone else’s.
Loro has built-in undo manager that handles this correctly.
4. Offline-First
// Save to IndexedDB
const snapshot = doc.exportSnapshot();
await idb.put('doc', snapshot);
// Restore on load
const saved = await idb.get('doc');
if (saved) doc.import(saved);
// Sync when online
socket.on('connect', () => {
socket.emit('sync', doc.exportFrom(lastSyncVersion));
});
Performance Considerations
- Document size: CRDTs store history; large docs get heavy
- Pruning: Periodically compact history on server
- Lazy loading: Only sync visible portions for large canvases
When to Use What
| Scenario | Recommendation |
|---|---|
| Text editor | Yjs (mature integrations) |
| Canvas/whiteboard | Loro (performance, tree support) |
| JSON documents | Automerge (familiar API) |
| Need P2P | Any CRDT |
| Simplicity over features | Server-authoritative (Figma-style) |
Sources:
Related: building figma today, rust wasm graphics