perf(extensions): avoid full-document scan for Placeholder when showOnlyCurrent is true - #7705
perf(extensions): avoid full-document scan for Placeholder when showOnlyCurrent is true#7705HMarzban wants to merge 2 commits into
Conversation
Port the O(depth) decoration path onto current Tiptap v3 placeholder in @tiptap/extensions (packages/extensions/src/placeholder).
When showOnlyCurrent is true (default), resolve the anchor and walk only the relevant depths instead of doc.descendants on every update. When showOnlyCurrent is false, keep a full-document scan for all empty textblocks.
Preserves upstream behavior: isNodeEmpty, textblock-only nodes, configurable dataAttribute / preparePlaceholderAttribute, and PluginKey('placeholder').
Made-with: Cursor
Made-with: Cursor
|
✅ Deploy Preview for tiptap-embed ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
| const isEmpty = !node.isLeaf && isNodeEmpty(node) | ||
|
|
||
| if (hasAnchor && isEmpty) { | ||
| decorations.push(createPlaceholderDecoration(editor, options, node, pos, hasAnchor, dataAttributeKey)) |
There was a problem hiding this comment.
Should you return early here? If I understand it correctly, the function only returns one decoration for the current node.
There was a problem hiding this comment.
We shouldn’t return early inside the loop.
The loop is over depths (1 … maxDepth). In principle more than one empty textblock on the resolved path could match (unusual schemas / nested textblocks). Stopping after the first push could drop a valid decoration, so we finish the loop.
On the API side: we collect a Decoration[] (length 0 or more) and pass it to DecorationSet.create(doc, decorations), which is what ProseMirror expects — an array of Decorations; an empty array is valid when there’s nothing to show.
Separately, the props.decorations hook may return null when the plugin is inactive (!active); that’s valid for “no decorations” and is separate from the Decoration[] passed into DecorationSet.create.
References: DecorationSet, EditorProps.decorations.
Summary
Optimizes
@tiptap/extensions/placeholderso that whenshowOnlyCurrentistrue(the default), placeholder decorations are built from the selection anchor only (O(depth)) instead of walking the entire document withdoc.descendantson every update (O(n)).When
showOnlyCurrentisfalse, behavior is unchanged in intent: we still perform a full-document scan so placeholders can appear on every empty textblock.Motivation
Large documents spend unnecessary work in the placeholder plugin on each transaction because the previous implementation always traversed the full node tree, even though the default configuration only needs the current empty block at the caret.
What changed
showOnlyCurrent: true: resolveanchor, walk relevant depths (includeChildrenstill respected viamaxDepth), only textblock nodes, same emptiness rules as before (isNodeEmpty).showOnlyCurrent: false: keep adoc.descendantspass for all empty textblocks (unavoidable for “show everywhere” semantics).PlaceholderOptions,dataAttribute/preparePlaceholderAttribute,PluginKey('placeholder'), and existing defaults.Performance
Testing / QA
Suggested manual checks:
showOnlyCurrent: false— placeholders on multiple empty blocks across the doc.showOnlyWhenEditable— matches prior behavior.dataAttribute— attribute still applied correctly.Risk / compatibility
Related file:
packages/extensions/src/placeholder/placeholder.ts