Content Check Performance in Large Collaborative Documents #8013
Replies: 1 comment
|
Your One difference worth reconsidering: the official On the debounce window: 1 second is a reasonable default, but since you already have This looks like a solid replacement for |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
The problem
I’m working on a collaborative writing app where some users create very large documents. One document that triggered this investigation was around 500k words.
Typing in that document was extremely laggy. Profiling showed the main cost came from enabling TipTap Collaboration’s content check:
enableContentCheck: trueThe Collaboration extension registers a
doc.on('beforeTransaction')handler that, on every Yjs transaction, converts the entireY.XmlFragmentto JSON, rebuilds the full ProseMirror document, and validates it:“Every Yjs transaction” includes every local keystroke and every received remote update. In a 500k-word document, that meant hundreds of milliseconds of full-document validation per keystroke, plus heavy GC pressure from repeatedly allocating full JSON and ProseMirror node trees. I measured around 15 seconds of GC per 10 seconds of typing, making the editor unusable.
What I observed
A few things stood out during the investigation:
The check seems unable to prevent invalid content. It runs in
beforeTransaction, so it validates the pre-update document state. Yjs handlers also cannot veto a transaction, since return values are ignored. So this appears to be detection/reporting only.Local transactions should already be valid. Content produced by the local editor should be schema-valid by construction, because ProseMirror enforces the schema when building transactions. The real risk seems to be content entering through Yjs externally, such as remote peers, server-side restores, or version-skewed clients.
The non-deprecated converter does not work for this use case.
yXmlFragmentToProsemirrorJSONis deprecated in favor ofyXmlFragmentToProseMirrorRootNode, but the replacement silently drops schema-unknown nodes, which defeats validation. The deprecated JSON converter is the only route I found that letsnodeFromJSON().check()actually see invalid content.enableContentCheckalso changessetContentbehavior. In@tiptap/core2.27.1,errorOnInvalidContentinsetContent/insertContentAtdefaults toeditor.options.enableContentCheck. TurningenableContentCheckoff also makes invalidsetContentinput stop throwing. Instead,createNodeFromContentwarns, returns empty content, andtr.replaceWithcan silently wipe the document. I had to explicitly pass{ errorOnInvalidContent: true }at import call sites.My replacement
I removed
enableContentCheckand added a small extension that keeps the same detection/reporting behavior, but only validates when it can matter.It:
fragment.observeDeepySyncPluginKeyY.UndoManagertransactionsonBeforeCreateto catch corrupt documents at loadImportant detail: use
onBeforeCreate, notonCreateThe initial validation has to run in
onBeforeCreate.During view binding,
y-prosemirrorcan silently delete nodes it cannot render. Specifically,createNodeFromYElementcatches the error and removes the node under theySyncPluginKeyorigin.So by
onCreate, a corrupt fragment may already look valid, and the data loss goes unreported. I have an integration test showing thatonCreateis too late.Result
Typing in the 500k-word document went from unusable back to normal.
I believe this keeps the same corruption detection coverage, and possibly improves it, because it catches corrupt-at-load documents before binding can mutate them.
Issues I can think of
Questions for the TipTap team
p.s. Im rather new to tiptap ecosystem so go easy :)
All reactions