docs/solutions/performance-issues/2026-05-23-slate-v2-core-operation-benchmarks-must-not-hide-snapshot-costs.md
The core operation lane was measuring the right user path, but some benchmark assertion helpers were reading full snapshots after each operation. That made small text writes look document-sized and hid whether the transaction fast lane actually helped.
core-huge-document-compare-local originally showed simple typing at tens of
milliseconds for 1000 blocks.core-observation-compare-local ran but reported
readChildrenLengthAfterEachMs around 36ms for 500 blocks after the first
transaction fix.normalization-compare-local reported insertTextReadAfterEachMs around
49ms even though the row only needed live children length after each insert.core-observation-compare-local failed in v2 with Node.nodes undefined
because v2 exposes NodeApi while legacy Slate exposes Node.Keep snapshot reads explicit. When a benchmark row only needs live children for assertions, prefer the live public API and fall back to snapshots only for older surfaces:
const getChildren = (editor) =>
typeof Editor.getChildren === 'function'
? Editor.getChildren(editor)
: typeof Editor.getSnapshot === 'function'
? Editor.getSnapshot(editor).children
: typeof editor.getChildren === 'function'
? editor.getChildren()
: editor.children
For compare scripts that traverse nodes, support both API names:
const NodeApi =
Slate.NodeApi ?? Slate.Node ?? SlateInternal.NodeApi ?? SlateInternal.Node
assert.ok(NodeApi?.nodes, 'Slate Node API with nodes() is required')
Lock the benchmark contract in tests by asserting that the helper checks
Editor.getChildren before Editor.getSnapshot, and that observation compare
uses the NodeApi fallback instead of destructuring Node.
Snapshot materialization is a valid thing to measure when the row is about snapshots. It is not valid when the row is supposed to measure text writes, children reads, normalization, or observation traversal.
Keeping live children reads first made the benchmark isolate the actual owner:
transaction/write cost. After the fix, the observation children row dropped from
about 36ms to about 2.8ms, and the normalization read-after-each row dropped
from about 49ms to about 3.6ms.
The NodeApi fallback keeps the same compare script runnable against legacy
Slate and v2 without making the benchmark package-specific.
Editor.getSnapshot.