Sorry Yjs, it's not you, it's us.
Like most editors, we started out with Yjs, and it was great: everything worked out of the box, with a large community, good documentation, and so on. But as we went deeper down the rabbit hole, striving to support documents with hundreds of pages, the cracks started to show… It’s not entirely Yjs’s fault. It is a genuinely great library, but it probably wasn’t built for this, and there’s a reason CRDTs aren’t the norm in large word processors: Google Docs famously uses operational transformation (OT) for collaborative editing.
Why not Yjs?
1 - Editing conflict resolution
CRDTs promise that concurrent edits always merge. They do merge, but not always into something a person would have written. Alex Clemmer at Moment covers this far better than we could: Part 1 shows how CRDTs and OT alike produce surprising results for offline conflicts, and Part 2 explains why they don’t use Yjs.
2 - We don’t need peer-to-peer
We’re building a professional document editor for organizations. Every document already lives on a server, behind permissions. Merging edits without a central authority is the main thing a CRDT buys you, and it’s not something we need. A server that orders every edit is simpler and easier to reason about.
3 - Remote changes replace the entire document
When another client makes a change, y-prosemirror produces a ProseMirror transaction that replaces the entire document (y-prosemirror#113 ). This causes all sorts of issues, from not being able to filter steps that only touch certain nodes, to decorations flickering as soon as another client makes a change. That’s terrible when you rely on decorations for things like proofreading.
4 - Quirks
Imagine editing a footnote (y-prosemirror#202 ). We naively left the selection on the footnote anchor, an inline atom node. When another client deleted the footnote, the editor crashed, leaving other clients with no way to delete it.
5 - Undo is painfully slow
Undo goes through the same path, so it redraws the entire document too. On a 200-page document, a single undo can take more than a second.
6 - Marks on nodes aren’t persisted
This one was annoying. Some nodes, like list numbers, need marks, but y-prosemirror doesn’t store marks on non-text nodes. So we had to write a plugin that converts those marks to attributes for persistence, and back to marks for display. You could argue “it’s open source, just open a PR”, but we’re not the only ones frustrated by this (y-prosemirror#47 ), and the issue was closed as “not planned”.
7 - The doc node can’t have attributes
Also closed as “not planned” (y-prosemirror#48 ). We get it, there are probably good reasons for it, and technical limitations the maintainers have had to deal with. But this is the second limitation that goes against ProseMirror’s design, and to be honest, we’d rather use ProseMirror the way it was designed than work around our collaboration layer. It had us, and others, considering an extra child node of the doc just to hold the doc’s attributes. Not a very elegant solution.
8 - Server-side edits aren’t straightforward
We generate and update documents from the server, for example filling in a template from a form or updating said document while users are in the room. With Yjs, the source of truth is the Y.Doc, not a ProseMirror document, so you can’t just apply a ProseMirror transaction. You either edit the Yjs types directly, bypassing your schema and commands, or convert the Y.Doc to ProseMirror, apply your transaction, and convert it back, letting y-prosemirror diff the result into the Y.Doc. Either way, the server speaks a different language than the editor and it’s yet another layer of complexity to deal with.
Conclusion
Yjs is a wonderful library, and for a lot of products it’s the right call. If you need peer-to-peer editing, or just want collaboration working this afternoon, use it.
We needed something else: a central authority, remote changes that arrive as regular ProseMirror steps, and a document model we don’t have to bend. That’s what Pitter-Patter gives us. It’s built by the team behind react-prosemirror, on top of prosemirror-collab-commit. Decorations stay put when others type, undo is plain prosemirror-history again, and node marks and doc attributes just work, because they’re ordinary ProseMirror steps. Server edits are ordinary ProseMirror transactions too.
So, sorry Yjs. It’s not you, it’s us.
This post reflects our experience with Yjs v13 and y-prosemirror, to the best of our knowledge. Yjs v14 is in the works and may address some of these points. If we got something wrong, let us know and we’ll correct it.