docs/solutions/performance-issues/2026-05-05-slate-v2-large-paste-fast-path-must-still-be-a-logical-operation.md
Large paste cannot be treated as repeated typing. The old multiline plaintext
fallback split text into lines, then looped through splitNodes and
insertText, creating thousands of operations for one user action.
The tempting fast path, direct snapshot replacement, is also wrong: it makes the tree change cheap, but history and operation observers see nothing.
editor.history.undos empty.insert_node work.Add a real child-window replacement operation and use it for high-pressure paste and exact whole-child range delete cases:
packages/slate/src/interfaces/operation.ts defines and inverts
replace_children.packages/slate/src/interfaces/transforms/general.ts applies
replace_children by replacing one child slice at the target path and setting
the new selection.packages/slate-dom/src/plugin/dom-clipboard-runtime.ts uses
replace_children for multiline plaintext paste into a single empty text
block.packages/slate-dom/src/plugin/dom-clipboard-runtime.ts converts multiline
plaintext paste in populated text-block targets into a normal model fragment
instead of looping line-by-line.packages/slate/src/transforms-text/insert-fragment.ts uses
replace_children for trusted Slate fragment paste into a single empty text
block and compatible, marked, inline-child, full-document text-block, and
selected whole top-level structural block targets.packages/slate/src/transforms-text/insert-fragment.ts fits top-level
multi text-block fragments into a populated text-block target as one child
window replacement while preserving surrounding text and selection.packages/slate/src/transforms-text/delete-text.ts uses replace_children
for exact whole top-level block range deletes instead of emitting repeated
remove_node operations.packages/slate/src/interfaces/node.ts slices exact whole top-level block
fragments directly so copy/cut of two blocks from a huge document does not
scan and prune every unselected sibling.packages/slate-dom/test/clipboard-boundary.ts locks selected inline-void
paste into a text target to one logical operation.packages/slate/test/collab-history-runtime-contract.ts proves
replace_children can be exported from a local paste or range-delete commit
and imported
through tx.operations.replay(...) with remote collaboration metadata.Lock it with behavior tests:
expect(operationCount).toBeLessThanOrEqual(1)
expect(editor.history.undos).toHaveLength(1)
Then prove the issue-size workload with a dedicated benchmark command:
bun run bench:slate:5945:issue
The verified 10,000-line plaintext paste row ran in 38.57ms with 1
operation.
The populated-editor #4056 issue rows now cover both sides of the old report:
copying 10,000 populated blocks ran in 12.16ms, and pasting 10,000 plaintext
lines into the middle of a 10,000-block populated editor ran in 185.49ms with
1 operation.
The #5992 issue-size cut row now separates the user interaction from cold
snapshot setup. On the exact 50,000-block shape, the warm edit lane ran in
9.95ms, warm copy-plus-delete ran in 8.62ms, and the edit emitted 1
operation. The cold first-edit row still records snapshot allocation separately
at 171.91ms. That split matters: interaction performance was fixed by
replace_children, while cold snapshot allocation remains a separate runtime
cost to keep visible.
Browser proof also needs an editor-scale row, not only a package benchmark. The generated editing stress suite covers a 5,000-block huge-document cut row plus the existing plaintext/richtext/forced-layout paste-normalize-undo rows:
STRESS_FAMILIES=huge-document-cut,paste-normalize-undo \
PLAYWRIGHT_RETRIES=0 \
bunx playwright test playwright/stress/generated-editing.test.ts \
-g "huge-document-cut|paste-normalize-undo" --project=chromium
Paste and exact whole-child range delete are each one user intention. Representing them as one child-window operation gives the runtime the fast path it needs without erasing history, selection repair, dirty-path classification, or future collaboration lowering.
Direct snapshot replacement is only safe for internal state reset paths. A user paste must remain observable as an edit.
For collaboration, replace_children is the Slate replay contract for child
window replacement. CRDT/Yjs adapters that cannot store subtree replacement
atomically should lower it at the adapter boundary, not by forcing paste or
range delete back through thousands of Slate operations.
docs/plans/2026-05-05-slate-v2-best-pasting-strategy-ralplan.mddocs/plans/2026-05-06-slate-v2-range-delete-replace-children-ralplan.mddocs/solutions/logic-errors/2026-05-04-clipboard-fragment-format-keys-must-guard-html-fallback.mddocs/solutions/logic-errors/2026-04-26-slate-browser-native-multiline-paste-success-must-block-fallback-insertion.mddocs/solutions/performance-issues/2026-04-11-slate-history-typing-bursts-need-legacy-style-merge-heuristics-before-anything-else.md