Skip to main content

Yjs vs Automerge vs Loro: CRDT Libraries 2026

·PkgPulse Team
0

CRDTs (Conflict-free Replicated Data Types) let multiple users edit the same data concurrently without a central coordination server. Three JavaScript libraries dominate this space: Yjs (the incumbent), Automerge (the research-backed alternative), and Loro (the Rust-powered newcomer). Each takes a fundamentally different approach to the same problem.

TL;DR

Yjs is the production default (~920K weekly downloads, 17K GitHub stars) with the largest ecosystem of bindings and providers. Automerge (~85K downloads) excels at document-level versioning with its Git-like change history. Loro (~12K downloads) is the fastest in benchmarks but youngest in ecosystem maturity. Pick Yjs unless you need Automerge's history model or Loro's raw performance.

Quick Comparison

YjsAutomergeLoro
Weekly Downloads~920K~85K~12K
GitHub Stars17K4.2K3.8K
Bundle Size (min+gz)~18 kB~320 kB (WASM)~180 kB (WASM)
LanguageJavaScriptRust + WASMRust + WASM
CRDT AlgorithmYATARGA + LWWFugue + Loro
Document HistoryLimitedFull Git-like DAGSnapshot-based
TypeScriptYesYesYes
LicenseMITMITMIT

Architecture: How They Differ

The three libraries solve the same fundamental problem — concurrent editing without conflicts — but with architecturally different approaches.

Yjs uses the YATA (Yet Another Transformation Approach) algorithm, implemented in pure JavaScript. Documents are represented as a linked list of operations. Each operation carries a unique ID (client + clock) and references its left and right neighbors at insertion time. When concurrent inserts target the same position, YATA uses a deterministic ordering rule based on client IDs to resolve the conflict.

The pure-JS implementation means Yjs has the smallest bundle size (18 kB gzipped) and no WASM startup cost. The trade-off is that very large documents (100K+ operations) get slower because garbage collection of the operation log is limited.

Automerge uses an RGA (Replicated Growable Array) for sequences and LWW (Last Writer Wins) registers for key-value data, compiled from Rust to WASM. The distinguishing feature is its change-based architecture: every edit creates a "change" object (analogous to a Git commit), and the document state is the result of applying all changes in causal order. This gives you free version history, branching, and merging.

The WASM compilation means a larger bundle (320 kB) and a one-time initialization cost (~50ms). But operations on large documents are faster because Rust's memory model handles the operation log more efficiently than JS garbage collection.

Loro is the newest entry, also Rust + WASM. It implements the Fugue algorithm (2023 paper) which provably achieves maximal non-interleaving — a property that prevents a specific class of editing anomalies that both YATA and RGA can produce in edge cases. Loro also introduces a novel encoding format that reduces document size by 2-5x compared to Yjs and Automerge for the same content.

Performance Benchmarks

Performance varies dramatically by operation type. These numbers come from the crdt-benchmarks suite (B4 benchmark: real-world editing trace from a 260K-character document):

OperationYjsAutomergeLoro
Apply 260K edits430ms680ms290ms
Encode document4ms12ms2ms
Decode document8ms45ms5ms
Document size (encoded)160 kB250 kB68 kB
Memory (loaded doc)28 MB41 MB15 MB

Loro leads in every category except initial WASM load time. Yjs is second overall and wins when bundle size matters (no WASM overhead). Automerge trades performance for its richer history model.

For most real-time collaborative applications (Google Docs-style editing, whiteboards, shared forms), all three are fast enough. The performance differences matter at scale: applications with 50+ concurrent editors, documents with 500K+ operations, or offline-first apps that need to merge large divergent histories.

Ecosystem and Bindings

This is where Yjs dominates. The Yjs ecosystem includes:

  • y-prosemirror / y-tiptap — ProseMirror and Tiptap rich-text bindings
  • y-codemirror.next — CodeMirror 6 binding
  • y-monaco — Monaco Editor binding
  • y-quill — Quill editor binding
  • y-websocket — WebSocket provider
  • y-webrtc — WebRTC peer-to-peer provider
  • y-indexeddb — IndexedDB persistence
  • y-redis — Redis-based persistence and scaling
  • Hocuspocus — Production WebSocket server for Yjs
  • Liveblocks — Managed Yjs backend (SaaS)
  • PartyKit — Edge-deployed Yjs provider
// Yjs + Tiptap example
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
import { useEditor } from '@tiptap/react';
import Collaboration from '@tiptap/extension-collaboration';
import CollaborationCursor from '@tiptap/extension-collaboration-cursor';

const ydoc = new Y.Doc();
const provider = new WebsocketProvider('wss://collab.example.com', 'doc-123', ydoc);

const editor = useEditor({
  extensions: [
    StarterKit.configure({ history: false }),
    Collaboration.configure({ document: ydoc }),
    CollaborationCursor.configure({ provider, user: { name: 'Alice', color: '#f00' } }),
  ],
});

Automerge has fewer bindings but covers the essentials:

  • @automerge/automerge-repo — Sync and storage layer
  • @automerge/prosemirror — ProseMirror binding (alpha)
  • automerge-repo-network-websocket — WebSocket sync
  • automerge-repo-storage-indexeddb — IndexedDB persistence

Loro's ecosystem is early-stage:

  • loro-prosemirror — ProseMirror binding
  • loro-crdt — Core CRDT library
  • Manual WebSocket integration required

For projects using Tiptap or CodeMirror (the two most popular collaborative editors), Yjs is the only library with mature, production-tested bindings. See also best real-time npm packages for the broader real-time ecosystem.

Document Model and Versioning

Automerge's strongest differentiator is its document model. Every change is a first-class object with a hash, dependencies (parent changes), timestamp, and actor ID. You can:

  • Fork a document, make changes, and merge back
  • View the document at any point in history
  • Diff between any two versions
  • Cherry-pick specific changes
import { next as Automerge } from '@automerge/automerge';

// Create and edit
let doc = Automerge.init<{ text: string; title: string }>();
doc = Automerge.change(doc, 'Set title', d => {
  d.title = 'Meeting Notes';
});
doc = Automerge.change(doc, 'Add text', d => {
  d.text = 'Discussed Q2 roadmap';
});

// View history
const history = Automerge.getHistory(doc);
// history[0].change.message === 'Set title'
// history[1].change.message === 'Add text'

// Fork and merge
let fork = Automerge.clone(doc);
fork = Automerge.change(fork, 'Edit on fork', d => {
  d.text = 'Discussed Q2 roadmap and hiring plan';
});
doc = Automerge.merge(doc, fork);

This Git-like model is valuable for applications where version history is a feature, not just an implementation detail: document editors with track changes, legal documents with audit trails, or configuration management systems.

Yjs has Y.UndoManager for undo/redo but no built-in history browsing or branching. Loro has snapshot-based versioning that's between the two — you can restore to previous snapshots but don't get Automerge's granular change DAG.

Integration Patterns

For most web applications, the integration pattern is:

  1. Editor binding — Connect the CRDT document to your text/code editor
  2. Sync provider — WebSocket, WebRTC, or managed service for real-time sync
  3. Persistence — Server-side storage for durability
// Yjs: Full stack with Hocuspocus
// Server
import { Hocuspocus } from '@hocuspocus/server';

const server = new Hocuspocus({
  port: 1234,
  async onStoreDocument({ documentName, document }) {
    await db.documents.upsert({
      where: { name: documentName },
      update: { content: Buffer.from(Y.encodeStateAsUpdate(document)) },
      create: { name: documentName, content: Buffer.from(Y.encodeStateAsUpdate(document)) },
    });
  },
  async onLoadDocument({ documentName, document }) {
    const stored = await db.documents.findUnique({ where: { name: documentName } });
    if (stored) Y.applyUpdate(document, stored.content);
  },
});

For deeper coverage of real-time infrastructure options (Liveblocks, PartyKit, Hocuspocus), see Liveblocks vs PartyKit vs Hocuspocus.

When to Use Which

Yjs — Default choice for collaborative editing. Best ecosystem, smallest bundle, good enough performance. Choose when you need Tiptap, CodeMirror, or Monaco bindings, or when bundle size matters (no WASM).

Automerge — Choose when version history is a product feature (not just a nice-to-have). Document branching, merging, and change attribution are built in. Accept the larger bundle size and smaller binding ecosystem.

Loro — Choose for performance-critical applications with large documents or many concurrent editors. Best raw performance and smallest encoded documents. Accept the early-stage ecosystem and plan to write custom integrations.

All three are MIT-licensed, actively maintained, and used in production. The CRDT space is maturing — the algorithmic differences matter less than the ecosystem and feature model you need.

The 2026 JavaScript Stack Cheatsheet

One PDF: the best package for every category (ORMs, bundlers, auth, testing, state management). Used by 500+ devs. Free, updated monthly.