docs/solutions/performance-issues/2026-03-31-slate-phase1-batch-lifecycle-should-land-before-fast-paths.md
Slate had already proved a real performance seam for repeated exact-path set_node updates, but the only fast implementation was a narrow exact-path batch prototype, which was too narrow to be the permanent design.
The missing piece was a generic batch lifecycle. Without that, every future optimization would stay a one-off API or a wrapper hack.
That seam also needs honest instrumentation. The rule is simple: public lanes measure wall time through the real engine, and any helper timings live in a separate breakdown pass.
editor.apply vs batch APIs" before there was any generic batch boundary to anchor the design.packages/slate/src/core/apply.tspackages/slate/src/editor/add-mark.tspackages/slate/src/editor/remove-mark.tspackages/slate/src/core/children.tsapplyBatch as a second plugin seam. That just recreates the same compatibility mess in a different shape.Land the generic lifecycle seam first:
Editor.withBatch(editor, fn)Transforms.applyBatch(editor, ops)Phase 1 behavior stays conservative on purpose:
Transforms.applyBatch(...) replays ordinary editor.apply(op) inside Editor.withBatch(...)onChange flush is deferred until the outer batch boundaryeditor.apply overrides still see each individual op in Phase 1Implementation shape:
packages/slate/src/utils/weak-maps.tspackages/slate/src/core/batch.tsEditor.withBatch(...) exported via packages/slate/src/editor/with-batch.tsTransforms.applyBatch(...) added in packages/slate/src/interfaces/transforms/general.tspackages/slate/src/core/apply.ts split into reusable internal phases:
apply path now dispatches explicitly between normal
single-op execution and batched execution instead of hiding that choice in one
monolithapply.tsadd-mark.tsremove-mark.tsnormalize.ts queues normalization instead of running it eagerly during a batchpackages/slate/test/perf/set-nodes-bench.js measures real wall time for the
public lanes and reports helper timings separatelyFocused regression coverage:
packages/slate/test/with-batch.tspackages/slate/test/apply-batch-exact-set-node.tspackages/slate-history/test/apply-batch-exact-set-node.tsThis slice solves the right problem first.
It does not try to beat the current optimized exact-path batch timings yet. It creates the engine contract that later fast paths can plug into without inventing another public special case.
It also proves an important compatibility point: Phase 1 batching can preserve the existing editor.apply override model because it still replays each op through editor.apply. That keeps the design stable while the lower-level batch executor is still being designed.
Editor.withBatch(...) as the lifecycle boundary and Transforms.applyBatch(...) as the execution entry point. Do not make both of them plugin override seams.editor.apply override compatibilityEditor.withBatch(...) loops that call custom editor.apply
wrappers and then read the updated treeapply, it is measuring the
harness, not Slate.