docs/plans/2026-04-21-slate-v2-react-huge-doc-perf-plan.md
/Users/zbeyens/git/plate-2/Users/zbeyens/git/slate-v2/Users/zbeyens/git/slatedocs/slate-v2/**, docs/slate-v2-draft/**, docs/plans/**packages/slate-react/** or packages/slate/** only after measured ownershipslate-react must beat legacy chunking-on and chunking-off on important huge-document user lanes without making child-count chunking foundational again.REACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:local5000 blocks is the closure gate for this lane.
1000 blocks is smoke/debug only and must not be cited as perf-superiority
proof.continue-perf, continue, vercel-react-best-practices, goal workflow, learnings-researcher, major-task.Fresh proof run:
REACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:local
Artifact:
/Users/zbeyens/git/slate-v2/tmp/slate-react-huge-document-legacy-compare-benchmark.jsonmiddleBlockPromoteThenTypeMs) against chunking-on by a small mean delta.
That is the accepted occlusion/corridor tradeoff, not a steady-editing loss.decorate story.slate-history or slate-hyperscript.The next implementation starts in:
packages/slateNot:
packages/slate-reactReason:
Current direct compare is useful but narrow:
Do not cut it. Reclassify it as the first subscribed-runtime compare gate, then add a browser user-lane gate after the core + React cuts move this command.
| Lane | Current read | Owner | Root cause | First fix |
|---|---|---|---|---|
| ready | v2 wins | keep as guardrail | shell/island mount avoids full legacy mount work | do not regress |
| start raw typing | v2 loses | packages/slate | Transforms.insertText enters withoutNormalizing, which forces whole-doc normalization; subscribed mode then pays snapshot/change publication | Phase 1 + 2 core |
| middle raw typing | v2 loses | packages/slate first, then slate-react | same core owner, plus far-shell preview selector execution when editing an unmounted island | Phase 1 + 2 core, then Phase 3 React selector prefilter |
| start select+type | v2 loses | mixed | selection publish + active island recompute + raw typing owner | Phase 2 core, then Phase 4 corridor |
| middle select+type | v2 loses hard | mixed, React after core | selection activates middle corridor, mounts more text nodes, shell previews/selectors execute, then raw typing owner fires | Phase 2 core, then Phase 3/4 React |
| select-all | v2 loses mildly | slate-react + core selection publish | shell-backed selection detection scans selected top-level range; selection change publication still pays snapshot/change cost | Phase 2 core, then Phase 4 shell-backed range metadata |
| full-document paste | v2 loses chunk-off, ties chunk-on | benchmark/core | current lane is Transforms.insertText over full selection, not real clipboard/fragment paste; core expanded delete/insert and publication dominate | after Phase 2, repair/extend benchmark before optimizing paste-specific code |
Hypothesis:
withoutNormalizing calls Editor.normalize(..., force: true) after every transform wrapper.Implementation target:
/Users/zbeyens/git/slate-v2/packages/slate/src/editor/without-normalizing.ts/Users/zbeyens/git/slate-v2/packages/slate/src/editor/normalize.ts/Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/transforms/text.tsPreferred fix:
Editor.normalize(editor, { explicit: false, force: false, operation: getLatestOperation(editor) })Editor.normalize(editor, { force: true }) behavior for deliberate whole-document normalization.Expected effect:
13ms to low single digits.Risk / blast radius:
packages/slate; normalization is correctness-sensitive.Proof gates:
bun test ./packages/slate/test/snapshot-contract.ts --bail 1
bun run bench:core:huge-document:compare:local
bun run bench:core:normalization:compare:local
bun run bench:core:observation:compare:local
Fallback / pivot:
withoutNormalizing change.TextTransforms.insertText that avoids forced full normalization only for collapsed point inserts with default normalization semantics.60%, the hypothesis failed; inspect Editor.void / Editor.elementReadOnly preflight and dirty-path update next.Hypothesis:
buildSnapshotChange(...) JSON-stringifies whole children/selection/marks.Implementation target:
/Users/zbeyens/git/slate-v2/packages/slate/src/core/public-state.ts/Users/zbeyens/git/slate-v2/packages/slate/src/core/apply.ts/Users/zbeyens/git/slate-v2/packages/slate/src/utils/runtime-ids.tsPreferred fix sequence:
buildSnapshotChange(...) full-tree JSON.stringify with operation-derived flags:
classes: ['text'], childrenChanged: true, dirty paths from op pathsclasses: ['selection'], childrenChanged: falseclasses: ['mark']Expected effect:
27ms toward single-digit ms.Risk / blast radius:
packages/slate; this is the core snapshot/store contract.SnapshotChange accuracy.Proof gates:
bun test ./packages/slate/test/snapshot-contract.ts --bail 1
bun run bench:core:huge-document:compare:local
bun run bench:core:observation:compare:local
bun run bench:react:rerender-breadth:local
REACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:local
Fallback / pivot:
childrenChanged/classes/dirtyPaths only and keep touched ids broad until a safer runtime-id resolver lands.Hypothesis:
SlateSelectorContext wakes every selector on every commit.Implementation target:
/Users/zbeyens/git/slate-v2/packages/slate-react/src/hooks/use-slate-selector.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/large-document/island-shell.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/projection-store.tsPreferred fix sequence:
SnapshotChange through the selector notification path.change.touchedRuntimeIds does not intersect the island runtime-id set.Vercel React rules applied:
rerender-derived-state: subscribe to derived dirtiness/top-level booleans, not whole snapshot facts.rerender-split-combined-hooks: split top-level id list, selection index, shell preview, and shell-backed selection checks.rerender-use-ref-transient-values: keep transient overlay/promotion inputs in refs where they are callback-only.js-set-map-lookups: prebuild island runtime-id Sets for touched-id intersection.js-combine-iterations: avoid shell preview loops that separately resolve kind, text, and changed-state.Expected effect:
Risk / blast radius:
slate-react; selector API is runtime-sensitive but package-local.Proof gates:
bun run bench:react:rerender-breadth:local
bun run bench:react:huge-document-overlays:local
REACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:local
Fallback / pivot:
Hypothesis:
Implementation target:
/Users/zbeyens/git/slate-v2/packages/slate-react/src/large-document/create-island-plan.ts/Users/zbeyens/git/slate-v2/packages/slate-react/src/large-document/large-document-commands.ts/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable.tsxPreferred fix sequence:
isSelectionShellBacked(...) answer range/intersection from intervals instead of looping every selected top-level block.Expected effect:
Risk / blast radius:
Proof gates:
bun run bench:react:huge-document-overlays:local
REACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:local
Correctness gate if touched:
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1
Fallback / pivot:
Hypothesis:
pasteFullDocumentMs is named too strongly. It measures full-selection Transforms.insertText(...), not browser clipboard transport or insertFragment.Implementation target:
/Users/zbeyens/git/slate-v2/scripts/benchmarks/browser/react/huge-document-legacy-compare.mjs/Users/zbeyens/git/slate-v2/playwright/integration/examples/** later for real browser proofPreferred fix sequence:
replaceFullDocumentWithTextMs.Expected effect:
Risk / blast radius:
Proof gates:
REACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:local
Fallback / pivot:
Do not call the lane closed until:
iterations >= 3bench:react:rerender-breadth:local stays greenbench:react:huge-document-overlays:local stays greenStart with Phase 1 in:
/Users/zbeyens/git/slate-v2/packages/slate/src/editor/without-normalizing.tsEarliest implementation gate stack:
bun test ./packages/slate/test/snapshot-contract.ts --bail 1
bun run bench:core:huge-document:compare:local
bun run bench:core:normalization:compare:local
bun run bench:core:observation:compare:local
docs/solutions/patterns/critical-patterns.md is absent.2026-04-11-slate-v2-react-perfect-runtime-opti-plan.md, 2026-04-11-slate-v2-semantic-islands-active-corridor-adaptive-occlusion-plan.md, 2026-04-11-slate-v2-large-document-broad-ops-batch.md, 2026-04-15-slate-v2-perf-architecture-research.md, and 2026-04-15-slate-v2-overlay-benchmark-hardening.md.chunking-review.md forbids reviving child-count chunking as the foundational answer; any large-doc optimization must be selector-first plus semantic islands, active corridor, occlusion, and optional planning geometry.architecture-contract.md requires committed snapshot reads, useSyncExternalStore selector subscriptions, narrow derived selectors, event-path writes before effect sync, and urgent typing/selection/DOM correctness staying synchronous.decoration-roadmap.md makes overlay architecture non-negotiable: one slate-react overlay kernel, separate decoration/annotation/widget lanes, source-scoped invalidation below React, and no broad text-tree churn from overlays.slate-react.huge-document-legacy-compare.mjs builds both repos, runs JSDOM/React act(...), and drives direct Transforms.* calls. It is valid for model + React commit/render work, not full browser keyboard/layout/default-paste latency.Editable with optional editor.getChunkSize and renderChunk; v2 surface uses EditableBlocks with largeDocument threshold 1, island size 100, active radius 1.EditableTextBlocks computes top-level runtime ids and selected top-level index via broad selector notifications. Large-doc islands are count-based groups today, not semantic block/list/table islands yet.LargeDocumentIslandShell; each shell subscribes and builds previews by resolving every runtime id in that island from snapshot.index.idToPath and walking descendants for preview text.useSlateSelector currently has one editor-scoped listener set; every commit calls every selector callback, then equality decides rerender. That preserves render locality but not selector execution locality.getSnapshot(...) clones/freeze children and rebuilds a full runtime-id index after cache invalidation. buildSnapshotChange(...) also JSON-stringifies previous/next children, selection, and marks. That cost is paid on committed text ops when listeners exist.Command:
CORE_HUGE_BENCH_BLOCKS=1000 CORE_HUGE_BENCH_ITERATIONS=1 CORE_HUGE_BENCH_TYPE_OPS=10 bun run bench:core:huge-document:compare:local
Artifact:
/Users/zbeyens/git/slate-v2/tmp/slate-core-huge-document-benchmark.jsonResult:
13.56ms, legacy 0.55ms, delta +13.01ms10.63ms, legacy 0.36ms, delta +10.27msRead:
packages/slate-react.One-off no-file-edit probe:
bun -e "import { createEditor, Transforms } from './packages/slate/src/index.ts'; ..."
Result on 1000 blocks / 10 middle-block inserts:
23.53mseditor.normalize = () => {}: 0.33mswithoutNormalizing that restores normalizing then calls normalize({ explicit:false, force:false }): 2.78msRead:
packages/slate/src/editor/without-normalizing.ts forcing full normalization after every Transforms.insertText(...).One-off no-file-edit probe with editor.subscribe(() => {}) before the same 1000-block / 10 middle-block insert loop:
54.46msnormalize = () => {}: 23.29ms27.02msRead:
apply.ts asks for a previous snapshot, applies the op, then publishes a next snapshot and builds a change. Current getSnapshot(...) clones/freezes the full children tree and rebuilds the full runtime-id index after cache invalidation; buildSnapshotChange(...) also JSON-stringifies previous and next children/selection/marks.Fresh bun run bench:react:rerender-breadth:local:
0; broad useSlate/selection hooks rerender by contract.1, sibling leaves 0, parent block 0.1, ancestors 0, sibling branch 0.Fresh bun run bench:react:huge-document-overlays:local:
1.74ms, active text renders 1, far renders 0.3.51ms, recompute count 1.7.20ms, selection lands at top-level 100.Read:
Verdict: replan
Harsh take: locality proof is already useful, but it does not prove the huge-doc latency claim; the red compare benchmark owns this task.
Claim: v2 slate-react huge-doc runtime beats legacy chunking-on/off on real user lanes.
Baseline matrix:
Win/loss:
Benchmark validity: suspect until benchmark code and runtime surfaces are inspected; useful enough to start, not enough to optimize blindly.
Ownership:
Earliest gates:
Next move: read source-of-truth docs, benchmark source, runtime source, and legacy huge-document source; then run the focused compare and classify owners.
Do not do:
Verdict: pivot
Harsh take: v2 still loses the important action lanes; ready wins do not buy perf superiority.
Claim: v2 slate-react huge documents beat legacy chunking-on/off on important huge-doc user lanes.
Baseline matrix:
EditableBlocks largeDocumentWin/loss:
Benchmark validity: valid for model + React/JSDOM commit cost; not valid as full browser keyboard/layout/paste latency proof.
Ownership:
Earliest gates:
bench:react:huge-document:legacy-compare:localNext move: turn the hot-path read into a phased optimization plan with proof commands and pivot criteria.
Do not do:
Verdict: pivot
Harsh take: the core is red enough that a React-only plan would be lying.
Claim: v2 huge-doc action latency can beat legacy only after the core text-op publish path stops burning about 10-13ms per 10-op burst.
Baseline matrix:
Win/loss:
Benchmark validity: valid for direct transform/model cost; still not browser latency.
Ownership:
Earliest gates:
bun test ./packages/slate/test/snapshot-contract.ts --bail 1 if core changesbun run bench:core:huge-document:compare:localNext move: inspect whether the core bill is snapshot cloning/index/change construction, then place React fixes after the core cut.
Do not do:
packages/slate-react until core drops materiallyVerdict: pivot
Harsh take: found the smoking gun: forced normalization dominates raw typing.
Claim: the first implementation starts in packages/slate, not packages/slate-react.
Baseline matrix:
Win/loss:
23.53ms0.33ms2.78msBenchmark validity: valid ownership probe for simple explicit-path text insertion; not a correctness proof.
Ownership:
Transforms.insertTextEarliest gates:
bun test ./packages/slate/test/snapshot-contract.ts --bail 1bun run bench:core:huge-document:compare:localNext move: final plan starts with a core normalization policy fix, then React selector/promotion work.
Do not do:
Verdict: pivot
Harsh take: after normalization, subscriber snapshot/change publication is the next wall.
Claim: raw typing needs two core cuts before React work: normalization policy, then listener publication cost.
Baseline matrix:
Win/loss:
2.78ms27.02ms23.29msBenchmark validity: valid owner probe for React-like subscribed editor commits.
Ownership:
Earliest gates:
bun test ./packages/slate/test/snapshot-contract.ts --bail 1bun run bench:core:huge-document:compare:localNext move: encode this as the final phased plan.
Do not do:
Verdict: replan
Harsh take: v2 wins mount, but it loses every important action lane at 5000 blocks; the perf-superiority claim is red, not merely incomplete.
Claim: v2 slate-react huge documents beat legacy chunking-on and
chunking-off on important huge-document user lanes.
Baseline matrix:
EditableBlocks largeDocumentWin/loss:
61.69ms342.84ms288.45ms227.89ms150.41ms27.93ms283.20ms176.92ms32.67ms263.84ms143.76ms45.69ms289.86ms159.30ms37.66ms35.76ms16.92ms1.75ms114.44ms105.02ms110.53msBenchmark validity:
Ownership:
Earliest gates:
bun test ./packages/slate/test/snapshot-contract.ts --bail 1bun run bench:core:huge-document:compare:localREACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:localNext move:
packages/slate/src/editor/without-normalizing.tsDo not do:
slate-react until core typing cost moves materiallyVerdict: pivot
Harsh take: the larger reference run makes the problem clearer, not better. v2's mount story is real, but the editing lanes are nowhere near legacy chunking-on.
Claim: v2 slate-react huge documents must beat legacy chunking-on and
chunking-off on important huge-document user lanes.
Baseline matrix:
EditableBlocks largeDocumentWin/loss:
60.17ms288.38ms287.75ms238.51ms143.59ms31.94ms309.88ms158.77ms28.28ms222.80ms145.71ms29.42ms290.34ms159.98ms29.54ms31.06ms13.36ms0.68ms120.99ms99.19ms114.54msBenchmark validity:
Ownership:
withoutNormalizing forcing whole-doc
normalization and subscribed snapshot/change publicationEarliest gates:
bun test ./packages/slate/test/snapshot-contract.ts --bail 1bun run bench:core:huge-document:compare:localREACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:localNext move:
packages/slate/src/editor/without-normalizing.ts by using
dirty-path normalization instead of forced full-document normalization after
ordinary transform wrappersDo not do:
Verdict: replan
Harsh take: maximum legacy API compatibility is useful only while it does not
poison the rewrite. If a compatibility-shaped API keeps getSnapshot(),
selector notification, or normalization semantics too expensive, preserving it
is the wrong goal.
Claim: v2 must deliver a better core/runtime/API for huge-doc editing than legacy chunking-on and chunking-off. Compatibility is bounded by that goal.
Baseline matrix:
Win/loss:
Benchmark validity:
Ownership:
Earliest gates:
bun test ./packages/slate/test/snapshot-contract.ts --bail 1bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1REACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:localNext move:
Editor.getSnapshot().Do not do:
Verdict: pivot
Harsh take: the kept core normalization cut was real; the React selector experiment was not. Keeping a non-moving selector patch would be benchmark theater, so it was cut back.
Claim: v2 slate-react huge docs must beat legacy chunking-on/off on important
5000-block user lanes.
Baseline matrix:
EditableBlocks largeDocumentWin/loss after kept core cuts:
70.89ms249.64ms287.49ms154.20ms149.69ms28.41ms198.01ms168.14ms32.62ms143.01ms145.52ms29.92ms230.15ms204.19ms26.85ms30.13ms17.51ms0.57ms116.64ms116.96ms121.14msBenchmark validity:
Ownership:
withoutNormalizing no longer force-normalizes whole document for
single text operationsSnapshotChange classification no longer stringifies full children for
ordinary op classesEditor.getSnapshot() is likely the wrong axisEarliest gates:
bun test ./packages/slate/test/snapshot-contract.ts --bail 1bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1bun run bench:react:rerender-breadth:localbun run bench:react:huge-document-overlays:localREACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:localNext move:
Do not do:
Editor.getSnapshot() as the hot read path if it blocks
chunking-on superiorityVerdict: keep course with blocker
Harsh take: the perf slice made a real core dent, but the lane is still red
against chunking-on. Typecheck is blocked by generated slate-dom declaration
names, not by the perf result.
Claim: kept changes must preserve correctness and keep the direct 5000-block compare honest while the next implementation pivots to a hard-cut read path.
Baseline matrix:
Win/loss:
bun test ./packages/slate/test/snapshot-contract.ts --bail 1: passbun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1: passbun test ./packages/slate-react/test/provider-hooks-contract.tsx --bail 1: passbun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1: passbun run bench:core:huge-document:compare:local: v2 still loses legacy core,
but the kept normalization cut holds the result near 3.8ms / 20 ops instead
of the old 10-13ms classbun run bench:core:normalization:compare:local: explicit normalization wins,
insertTextReadAfterEachMs remains redbun run bench:core:observation:compare:local: positions/read observation
remains redbun run bench:slate:6038:local: passbun run bench:react:rerender-breadth:local: locality restored after cutting
the failed selector experimentbun run bench:react:huge-document-overlays:local: pass as guardrailbunx turbo build --force --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-react: passbunx turbo typecheck --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-react: blocked by generated
packages/slate-dom/dist/index.d.ts references to unaliased BaseEditor,
Editor, and Ancestorbun run lint:fix: pass, fixed one filebun run lint: passBenchmark validity:
Ownership:
slate-dom declaration bundling/typecheckNext move:
slate-dom declaration bundler issue before
claiming package typecheck closure.Do not do:
slate-dom source just to appease generated d.tsVerdict: pivot
Harsh take: the 1000-block smoke is less brutal than 5000, but it still rejects the idea that the lane is closed. v2 wins mount and nearly ties start typing, then still bleeds on middle typing, middle select+type, select-all, and paste.
Claim: v2 slate-react must beat legacy chunking-on/off on important huge-doc
user lanes, not just prove locality or command ownership.
Baseline matrix:
EditableBlocks largeDocumentWin/loss:
29.42ms50.57ms66.85ms38.93ms38.71ms30.44ms45.74ms31.76ms43.61ms44.00ms26.92ms25.79ms77.24ms27.38ms27.29ms10.00ms2.95ms2.76ms26.52ms20.81ms24.70msBenchmark validity:
Ownership:
Earliest gates:
bun test ./packages/slate/test/snapshot-contract.ts --bail 1bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1REACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:localNext move:
Do not do:
getSnapshot() reads in urgent node/text
rendering if direct live path reads are fasterVerdict: keep course
Harsh take: the React hot-read cuts finally moved the real lanes, but 5000-block chunking-on still exposes that v2 is not done.
Claim: v2 slate-react huge documents beat legacy chunking-on/off on important
user lanes.
Baseline matrix:
EditableBlocks largeDocumentHypotheses tested:
Editable should not broadly rerender for text ops; it should rerender only
for selection/structural opsSlate should read only the new operation slice instead of cloning full
operation history per commitFiles changed:
/Users/zbeyens/git/slate-v2/packages/slate-react/src/hooks/use-slate-selector.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/slate.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/large-document/island-shell.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/large-document/create-island-plan.ts/Users/zbeyens/git/slate-v2/packages/slate-react/src/large-document/large-document-commands.ts/Users/zbeyens/git/slate-v2/packages/slate/src/core/public-state.ts/Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/editor.tsRejected / cut tactics:
Editable using useSlateStatic() alone was too aggressive; it broke
selection scrolling and structural redecorations, so the kept cut adds a
narrow non-text subscriptionCommands and artifacts:
bun test ./packages/slate/test/snapshot-contract.ts --bail 1
190 testsbun test ./packages/slate/test/surface-contract.ts --bail 1
7 testsbun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1
5 testsbun test ./packages/slate-react/test/provider-hooks-contract.tsx --bail 1
4 testsbun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
4 testsbun test ./packages/slate-react/test/editable-behavior.tsx --bail 1
3 testsbun run bench:slate:6038:local
withTransactionMeanMs 0.0886, applyBatchMeanMs 0.0842bun run bench:core:huge-document:compare:local
/Users/zbeyens/git/slate-v2/tmp/slate-core-huge-document-benchmark.json4.06ms vs 0.73ms, middle 3.96ms
vs 0.54msbun run bench:core:normalization:compare:local
/Users/zbeyens/git/slate-v2/tmp/slate-core-normalization-benchmark.jsonbun run bench:core:observation:compare:local
/Users/zbeyens/git/slate-v2/tmp/slate-core-observation-benchmark.jsonpositionsFirstBlockAfterEachMsbun run bench:react:rerender-breadth:local
/Users/zbeyens/git/slate-v2/tmp/slate-react-rerender-breadth-benchmark.json0bun run bench:react:huge-document-overlays:local
/Users/zbeyens/git/slate-v2/tmp/slate-react-huge-document-overlays-benchmark.json1.26msREACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:local
/Users/zbeyens/git/slate-v2/tmp/slate-react-huge-document-legacy-compare-benchmark.json33.21ms vs chunk-off 55.07ms / chunk-on 88.92ms31.69ms vs 30.15ms / 36.51ms34.73ms vs 36.56ms / 49.11ms32.60ms vs 29.58ms /
28.77ms30.55ms vs 30.11ms /
31.03ms7.94ms vs 3.20ms / 2.91ms35.73ms vs 22.95ms / 26.18msREACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:local
/Users/zbeyens/git/slate-v2/tmp/slate-react-huge-document-legacy-compare-benchmark.json45.16ms vs 287.84ms / 329.80ms138.60ms vs
148.13ms / 33.44ms148.81ms vs
163.68ms / 38.87ms150.12ms vs
184.23ms / 39.67ms159.22ms vs
161.79ms / 35.04ms19.88ms vs 13.95ms / 1.13ms158.46ms vs 103.99ms / 117.74msbun run lint:fix
3 filesbun run lint
bunx turbo build --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-react
bunx turbo typecheck --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-react
packages/slate-dom/dist/index.d.ts aliasing
(BaseEditor, Editor, Ancestor); new slice TS errors were fixedBenchmark validity:
Ownership:
Earliest gates:
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1bun test ./packages/slate/test/snapshot-contract.ts --bail 1REACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:localREACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:localNext move:
Do not do:
Verdict: keep course
Harsh take: typing is finally near the right shape at 1000 blocks, but paste and select-all are still red; the old paste row was hiding two different workloads.
Claim: v2 slate-react huge documents beat legacy chunking-on/off on important
user lanes.
Baseline matrix:
EditableBlocks largeDocumentHypotheses tested:
pasteFullDocumentMs row is too muddy because it measures
full-selection insertText, not fragment pasteEditor.getSnapshot() or per-block
mounted-id lookup; live selection plus mounted intervals is enoughFiles changed:
/Users/zbeyens/git/slate-v2/scripts/benchmarks/browser/react/huge-document-legacy-compare.mjs/Users/zbeyens/git/slate-v2/packages/slate-react/src/large-document/large-document-commands.ts/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsxCommands and artifacts:
REACT_HUGE_COMPARE_BLOCKS=200 REACT_HUGE_COMPARE_ITERATIONS=1 REACT_HUGE_COMPARE_TYPE_OPS=5 bun run bench:react:huge-document:legacy-compare:local
/Users/zbeyens/git/slate-v2/tmp/slate-react-huge-document-legacy-compare-benchmark.jsonreplaceFullDocumentWithTextMsinsertFragmentFullDocumentMsbun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1
REACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:local
/Users/zbeyens/git/slate-v2/tmp/slate-react-huge-document-legacy-compare-benchmark.json29.02ms vs chunk-off 47.63ms / chunk-on 69.87ms28.58ms vs 28.40ms / 30.89ms30.37ms vs 30.83ms / 47.33ms29.49ms vs 28.65ms / 25.98ms29.29ms vs 28.41ms / 27.36ms5.51ms vs 2.83ms / 2.70ms30.69ms vs 19.36ms /
25.09ms29.43ms vs 19.92ms /
20.33msbun run lint:fix
bun run lint
bunx turbo build --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-react
bunx turbo typecheck --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-react
slate-dom/dist/index.d.ts aliasing
problem (BaseEditor, Editor, Ancestor)Benchmark validity:
Ownership:
Earliest gates:
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1REACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:localREACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:localNext move:
replaceFullDocumentWithTextMs and insertFragmentFullDocumentMs
separatelyDo not do:
Verdict: pivot
Harsh take: paste is no longer the problem; the hard remaining loss is mounted active text editing at 5000 blocks.
Claim: v2 slate-react huge documents beat legacy chunking-on/off on important
user lanes.
Baseline matrix:
EditableBlocks largeDocumentHypotheses tested:
set_selection was doing unnecessary whole-document transaction
bootstrapFiles changed:
/Users/zbeyens/git/slate-v2/scripts/benchmarks/core/compare/huge-document.mjs/Users/zbeyens/git/slate-v2/scripts/benchmarks/browser/react/huge-document-legacy-compare.mjs/Users/zbeyens/git/slate-v2/packages/slate/src/interfaces/transforms/text.ts/Users/zbeyens/git/slate-v2/packages/slate/src/transforms-text/insert-fragment.ts/Users/zbeyens/git/slate-v2/packages/slate/src/core/apply.ts/Users/zbeyens/git/slate-v2/packages/slate-react/src/large-document/create-island-plan.ts/Users/zbeyens/git/slate-v2/packages/slate-react/src/large-document/island-shell.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/large-document/large-document-commands.ts/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx/Users/zbeyens/git/slate-v2/packages/slate-react/test/large-doc-and-scroll.tsxCommands and artifacts:
CORE_HUGE_BENCH_BLOCKS=1000 CORE_HUGE_BENCH_ITERATIONS=1 CORE_HUGE_BENCH_TYPE_OPS=10 bun run bench:core:huge-document:compare:local
/Users/zbeyens/git/slate-v2/tmp/slate-core-huge-document-benchmark.json5.53ms vs legacy 13.28ms5.32ms vs legacy 9.20msCORE_HUGE_BENCH_BLOCKS=5000 CORE_HUGE_BENCH_ITERATIONS=1 CORE_HUGE_BENCH_TYPE_OPS=10 bun run bench:core:huge-document:compare:local
/Users/zbeyens/git/slate-v2/tmp/slate-core-huge-document-benchmark.json21.12ms vs legacy 75.24ms20.68ms vs legacy 68.34ms0.01ms vs legacy 0.02msbun test ./packages/slate/test/snapshot-contract.ts --bail 1
bun test ./packages/slate/test/surface-contract.ts --bail 1
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1
bun test ./packages/slate-react/test/provider-hooks-contract.tsx --bail 1
bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
bun run bench:react:huge-document-overlays:local
/Users/zbeyens/git/slate-v2/tmp/slate-react-huge-document-overlays-benchmark.json11.9msREACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:local
/Users/zbeyens/git/slate-v2/tmp/slate-react-huge-document-legacy-compare-benchmark.json23-36ms2-4ms3ms24-31ms37.67ms32.90ms37.25ms3.61ms3.95ms2.81ms5.76ms vs 29.91ms / 33.22ms9.00ms vs 23.11ms / 22.03msREACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:local
/Users/zbeyens/git/slate-v2/tmp/slate-react-huge-document-legacy-compare-benchmark.json18.20ms269.06ms311.91ms134.68ms vs chunk-on 34.11ms129.94ms vs chunk-on 29.65ms145.17ms vs chunk-on 45.18ms20.13ms0.61msBenchmark validity:
middleBlockTypeMs and middleBlockSelectThenTypeMs are model-only for v2
when the block is shelled; the repaired user corridor row is
middleBlockPromoteThenTypeMsOwnership:
Earliest gates:
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1bun test ./packages/slate/test/snapshot-contract.ts --bail 1REACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:localREACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:localNext move:
act/JSDOM dominatesDo not do:
Verdict: keep course -> accepted tradeoff closure
Harsh take: v2 now wins the real steady-state lanes; the only remaining slower row is first activation of an occluded block, which is the intended cost of occlusion, not a reason to revive chunking.
Claim: v2 slate-react huge documents beat legacy chunking-on/off on important
user lanes, with first activation of a shelled block explicitly accepted as the
occlusion tradeoff.
Baseline matrix:
EditableBlocks largeDocumentFinal win/loss:
middleBlockPromoteThenTypeMs against chunking-on by about
9.13ms mean / 3-7ms median-class cost, depending on sample shapeAccepted tradeoff:
middleBlockPromoteThenTypeMs includes first activation of a shelled,
occluded block.Key changes landed:
Editable no longer broad-rerenders for text opsactiveRadius default is 0middleBlockPromoteThenTypeMs to avoid hiding activation
cost inside model-only typingCommands and artifacts:
bun test ./packages/slate/test/snapshot-contract.ts --bail 1
bun test ./packages/slate/test/surface-contract.ts --bail 1
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1
bun test ./packages/slate-react/test/provider-hooks-contract.tsx --bail 1
bun test ./packages/slate-react/test/projections-and-selection-contract.tsx --bail 1
bun run bench:react:rerender-breadth:local
0 for directly synced text ops/Users/zbeyens/git/slate-v2/tmp/slate-react-rerender-breadth-benchmark.jsonbun run bench:react:huge-document-overlays:local
1, promotion mean about 1.83ms/Users/zbeyens/git/slate-v2/tmp/slate-react-huge-document-overlays-benchmark.jsonREACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:local
/Users/zbeyens/git/slate-v2/tmp/slate-react-huge-document-legacy-compare-benchmark.jsonbun run lint:fix
bun run lint
bunx turbo build --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-react
bunx turbo typecheck --filter=./packages/slate --filter=./packages/slate-dom --filter=./packages/slate-react
packages/slate-dom/dist/index.d.ts aliasing issue:
BaseEditorEditorAncestorBenchmark validity:
Ownership:
slate-dom d.ts aliasing, not this perf laneEarliest gates:
bun test ./packages/slate-react/test/large-doc-and-scroll.tsx --bail 1bun test ./packages/slate/test/snapshot-contract.ts --bail 1REACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:localNext move:
slate-dom/dist/index.d.ts aliasing so
package typecheck can close.Do not do:
slate-dom generated d.ts blocker a perf failure