docs/solutions/performance-issues/2026-03-31-plate-nodeid-should-use-setnodesbatch-only-for-live-normalization.md
nodeId should use setNodesBatch only for live normalizationPlate needed the new batched set_node fast path inside its local
@platejs/slate package, but nodeId had two different workloads:
editor.childrenThose workloads look similar on paper and they are not the same seam. Reusing the public batch API for both would have made initialization noisier and less honest.
nodeId already had a good pure initial-value path through
transformInitialValue, but the live nodeId.normalize() transform still
paid the per-node setNodes cost.setNodesBatch API lived in Plate's local packages/slate, not only
in the separate .tmp/slate-v2 prototype repo, so the adoption work could happen
immediately.nodeId initial-value transform with
editor.tf.setNodesBatch(...). That would manufacture operations, history
boundaries, and change notifications during initialization even though the
plugin already owns the initial value.@platejs/slate package could safely deep-import Slate
private internals for dirty-path updates. The published slate package ships
a bundled runtime, not a clean public deep-import surface for those helpers.Keep the abstractions honest.
editor.tf.setNodesBatch(...) to @platejs/slateThe local Slate package now exposes an explicit exact-path batch API. It keeps
the one-pass tree rewrite from the upstream prototype and records ordinary
set_node operations for history and change detection.
The focused tests live in:
withHistory now understands setNodesBatch as one logical change:
withNewBatchwithoutSavingThat keeps the batch API fast without pretending it is the same thing as a loop
of ordinary apply(...) calls.
nodeId.normalize()The nodeId plugin now collects missing-id updates first and applies them with
one editor.tf.setNodesBatch(...) call under withoutSaving.
transformInitialValue stays on the pure returned-value path, controlled by the
initialValueIds option.
That split matters:
The speedup comes from rewriting shared ancestors once instead of once per
set_node operation.
The semantic win comes from not overusing that API. setNodesBatch is a
runtime transform surface. Initial normalization is not.
The local Plate port keeps the fast rewrite, saves history explicitly, and runs a local dirty-path normalization queue for the batch. That keeps the feature production-safe without lying about access to Slate internals.
The local micro-benchmark kept the real performance win on a flat huge-document shape:
| Blocks | setNodes | setNodesBatch | Speedup |
|---|---|---|---|
1000 | 18.56 ms | 2.63 ms | 7.05x |
5000 | 118.54 ms | 4.92 ms | 24.10x |
editor.children during initialization, do not
route that work back through editor operations just to reuse a runtime API.