docs/plans/2026-05-23-slate-v2-large-document-performance-virtualization-ralplan.md
Keep the current architecture direction. Do not rewrite large-document rendering around virtualization as the default.
The best long-term shape is:
auto: DOM-present staged rendering with separate
interactiveReady and nativeSurfaceComplete metrics;staged: force the safe large-document DOM-present path;full: debug and comparison path;virtualized: pathological-document mode with named
native-behavior limits;slate-layout / Pretext snapshots feed layout sizes and hit regions,
but Slate React still owns DOM materialization, selection import/export, DOM
coverage, copy/paste, IME, and mobile proof.TanStack Virtual is an internal viewport/range engine for virtualized. It
should not leak into Slate's public API.
Create the execution lane for the remaining large-document performance and virtualization issue family:
#5945, #4056, #5992, #2051, and
#790..tmp/slate-v2.#790 is fixed or improved.Fixes #5945, Fixes #4056, or Fixes #5992 promotion.Read surfaces:
docs/plans/2026-05-01-slate-v2-universal-large-document-performance-ralplan.mddocs/slate-v2/replacement-gates-scoreboard.mddocs/slate-v2/slate-react-perf-loop-context.mddocs/slate-issues/benchmark-candidate-map.mddocs/slate-issues/gitcrawl-v2-sync-ledger.mddocs/slate-v2/ledgers/fork-issue-dossier.mddocs/slate-v2/ledgers/issue-coverage-matrix.mddocs/slate-v2/references/pr-description.md.tmp/slate-v2/site/examples/ts/huge-document.tsx.tmp/slate-v2/packages/slate-react/src/dom-strategy/use-virtualized-root-plan.ts.tmp/slate-v2/packages/slate-react/src/components/editable-text-blocks.tsx.tmp/slate-v2/scripts/benchmarks/browser/react/huge-document-legacy-compare.mjs.tmp/slate-v2/playwright/integration/examples/huge-document.test.ts.tmp/slate-v2/docs/libraries/slate-react/experimental-virtualized-rendering.mdReusable learnings applied:
virtualizer.getVirtualItems() by virtualizer identity.| Issue | Current claim | This plan |
|---|---|---|
#5945 slow large plaintext paste | Improves | Preserve. Issue-size 10,000-line plaintext paste is one logical operation. Exact browser reproduction closure still needs a 10,000-line browser artifact. |
#4056 copy/paste very large text | Improves | Preserve. Populated 10,000-block copy and 10,000-line middle paste have benchmark proof; exact full-book browser reproduction remains open. |
#5992 huge-document cut cost | Improves | Preserve. 10,000-block cut remains within the issue target thresholds, but the fresh 50,000-block artifact is red while still preserving one logical operation. Exact closure remains backlog. |
#2051 leaf rerender breadth | Related / performance guardrail | Keep as guardrail. Rerender breadth is represented by benchmark gates, not exact issue closure. |
#790 dynamic rendering | Related proof-route backlog | Keep backlog. Virtualized rendering directly targets the problem, but claim requires mount/edit/scroll benchmark, DOM coverage proof, and browser native-behavior proof. |
No new fixed issue claims. No new improved issue claims.
| Option | Decision | Reason |
|---|---|---|
| Make virtualization the default | Reject | It breaks native full-document DOM assumptions for browser find, screen readers, selection, clipboard, IME, and mobile unless the editor owns replacements for all of them. |
| Keep DOM-present staged as default | Accept | It preserves native behavior while making the 5000-block release target fast enough. |
Keep virtualized as explicit experimental object mode | Accept | It is the right pathological-document escape hatch and the right place to use TanStack Virtual. |
| Expose TanStack options publicly | Reject | That makes Slate's API depend on a list virtualizer instead of editor behavior. |
Feed virtualizer sizes from slate-layout / Pretext | Accept as future target | Layout-derived sizes are better than estimates, but DOM coverage and selection policy remain Slate-owned. |
The default 5000-block release gate is good enough to claim within scope:
v2DefaultOmitted ready around 19.44ms;8.92ms;31.55ms;35.93ms;14ms or less in the current coalesced/default family;295ms ready, 35ms type, and 31-35ms
selection/promotion rows depending on run.The 10000-block stress gate is still red:
v2DefaultOmitted select/type around 69.03ms;v2DefaultOmitted promote/type around 72.29ms;34.70ms;35.75ms.Do not hide that. The next owner is 10000-block selection-inclusive materialization/selection repair cost, not raw typing and not local group-size tuning.
Keep:
<Editable domStrategy="auto" />
<Editable domStrategy="staged" />
<Editable domStrategy="full" />
<Editable
domStrategy={{
estimatedBlockSize: 32,
overscan: 4,
threshold: 25_000,
type: 'virtualized',
}}
style={{ height: 480, overflowY: 'auto' }}
/>
Rules:
auto is the default and remains DOM-present first.staged is the explicit safe large-document path.full is debug/comparison.virtualized stays object-only and experimental.getScrollElement, measureElement, rangeExtractor, item key,
or TanStack virtualizer instance.Editable; callers should
not need useMemo for stable behavior.EditableTextBlocks owns the materialization plan:
useVirtualizedRootPlan owns only the virtual range:
virtualizer.getVirtualItems() live during render;slate-layout / Pretext future:
| Cohort | Blocks | Default posture |
|---|---|---|
| normal | 0-500 | Full DOM, no large-doc behavior needed. |
| medium | 500-2000 | auto can remain full or staged depending on threshold. |
| large | 2000-10000 | auto uses staged DOM-present with eventual DOM coverage. |
| stress | 10000-50000 | Keep default staged; run stress gates separately. |
| pathological | 50000+ | virtualized may be used explicitly with degraded-mode labeling. |
Repeated unit: top-level block/root group.
Target budget:
Required rows:
Report p50/p75/p95/p99 where the harness supports it. Do not use a single mean as a release argument.
Every perf artifact for this lane must include:
staged:
virtualized:
Owner: benchmark/docs.
v2DefaultOmitted,
v2DefaultRenderAuto, v2AutoExplicit, v2DomPresent,
v2VirtualizedExperimental, and no stale v2NoIsland.domStrategyType is present in every huge-doc trace.virtualizationEnabled: true.Verification:
cd .tmp/slate-v2
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
Owner: slate-react performance.
25 and immediate background
mounting were already rejected.Verification:
cd .tmp/slate-v2
REACT_HUGE_COMPARE_BLOCKS=10000 REACT_HUGE_COMPARE_ITERATIONS=5 REACT_HUGE_COMPARE_TYPE_OPS=10 bun run bench:react:huge-document:legacy-compare:local
Owner: slate-react DOM strategy.
virtualizer.getVirtualItems() live, not memoized by virtualizer
identity.estimatedBlockSize.Verification:
cd .tmp/slate-v2
cd packages/slate-react
bun run test:vitest test/dom-strategy-and-scroll.test.tsx
cd ../..
PLAYWRIGHT_RETRIES=0 bunx playwright test playwright/integration/examples/huge-document.test.ts --project=chromium --grep "virtualized|Huge Document"
Owner: slate-browser / slate-react.
Add or refresh browser rows for:
Verification:
cd .tmp/slate-v2
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
Owner: slate core / slate-dom.
Refresh issue-size clipboard artifacts before any maintainer-facing claim text:
cd .tmp/slate-v2
bun run bench:slate:5945:issue
SLATE_CLIPBOARD_BENCH_HUGE_CUT_BLOCKS=50000 SLATE_CLIPBOARD_BENCH_ISSUE_TARGETS=1 bun ./scripts/benchmarks/slate/5945-large-plaintext-paste.mjs
Keep the claims:
Improves #5945;Improves #4056;Improves #5992.Do not promote them to Fixes without browser reproduction acceptance.
Required before the lane can be marked done:
cd .tmp/slate-v2
bun run bench:react:rerender-breadth:local
bun run bench:react:huge-document-overlays:local
CORE_HUGE_BENCH_LEGACY_REPO=<legacy-slate-checkout> bun run bench:core:huge-document:compare:local
bun lint:fix
bun typecheck:root
bun check
Run bun check:full only if the execution slice makes browser/release-quality
claims that need the full local browser sweep.
| Lens | Status | Notes |
|---|---|---|
| slate-ralplan | applied | Keeps implementation untouched and routes execution to Ralph. |
| clawsweeper | applied | Issue accounting is ledger-first; no broad live GitHub sweep. |
| performance | applied | Cohorts, repeated-unit budget, INP rows, memory tags, degradation contract, and native behavior gates are explicit. |
| tanstack-virtual | applied | TanStack is internal range/measurement engine only. |
| learnings-researcher | applied | Existing perf solution notes were checked before writing the lane. |
| goal workflow | applied | This file is the durable plan artifact. |
| tdd | deferred to Ralph | Planning pass only. Execution must add/refresh focused tests before code changes. |
| visual/browser proof | deferred to Ralph | Required for virtualized and native behavior claims. |
Overall score: 0.89.
Breakdown:
| Criterion | Score | Reason |
|---|---|---|
| Architecture clarity | 0.93 | Boundaries are clear: layout, DOM materialization, virtualization, selection, and metrics have separate owners. |
| DX | 0.90 | Public API stays editor-shaped and avoids TanStack leakage. |
| Performance strategy | 0.91 | Cohorts and interaction rows are explicit; 10000 stress debt is not hidden. |
| Native behavior safety | 0.84 | Good contract, but virtualized mode still needs more browser/mobile/IME proof. |
| Issue accounting | 0.91 | Existing Improves/Related status is preserved with exact non-claim boundaries. |
| Execution readiness | 0.87 | Commands and phases are concrete; runtime proof still belongs to the next Ralph pass. |
Ralph execution patched the current huge-document React benchmark harness and closed the verification lane with scoped proof.
Implementation changes in .tmp/slate-v2:
scripts/benchmarks/browser/react/huge-document-legacy-compare.mjs imports
createReactEditor from the current slate-react package instead of the
removed withReact wrapper, replaces benchmark editor creation accordingly,
and restores the missing shellEnabled trace flag.packages/slate-react/src/hooks/use-slate-history.ts avoids React ref
reads/writes during render by keeping the last selected history root inside a
stable runtime selector.Fresh proof:
REACT_HUGE_COMPARE_MODE=current-only REACT_HUGE_COMPARE_SURFACES=v2DefaultOmitted REACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=1 REACT_HUGE_COMPARE_TYPE_OPS=2 bun run bench:react:huge-document:legacy-compare:local
tmp/slate-react-huge-document-legacy-compare-benchmark-current-only-v2DefaultOmitted-blocks-5000-iters-1-ops-2-combined-selection-no-profile.jsonREACT_HUGE_COMPARE_MODE=current-only REACT_HUGE_COMPARE_SURFACES=v2DefaultOmitted REACT_HUGE_COMPARE_BLOCKS=10000 REACT_HUGE_COMPARE_ITERATIONS=1 REACT_HUGE_COMPARE_TYPE_OPS=2 bun run bench:react:huge-document:legacy-compare:local
tmp/slate-react-huge-document-legacy-compare-benchmark-current-only-v2DefaultOmitted-blocks-10000-iters-1-ops-2-combined-selection-no-profile.json150.17ms, middle select/type
306.44ms, middle promote/type 280.46ms, native surface complete
3113.55ms.REACT_HUGE_COMPARE_MODE=current-only REACT_HUGE_COMPARE_SURFACES=v2VirtualizedExperimental REACT_HUGE_COMPARE_BLOCKS=5000 REACT_HUGE_COMPARE_ITERATIONS=1 REACT_HUGE_COMPARE_TYPE_OPS=2 bun run bench:react:huge-document:legacy-compare:local
tmp/slate-react-huge-document-legacy-compare-benchmark-current-only-v2VirtualizedExperimental-blocks-5000-iters-1-ops-2-combined-selection-no-profile.jsonnativeSurfaceCompleteAt: null, virtualizationEnabled: true.bun run bench:react:rerender-breadth:local
0 for the key repeated-unit
rows.bun run bench:react:huge-document-overlays:local
CORE_HUGE_BENCH_LEGACY_REPO=/Users/zbeyens/git/slate bun run bench:core:huge-document:compare:local
86.95ms / 83.78ms versus legacy around
0.72ms / 0.68ms.bun run bench:slate:5945:issue
51.45ms, populated full-selection copy 46.33ms,
populated middle paste into 10,000 blocks 248ms, 10,000-block two-node
cut thresholds passed with one operation.SLATE_CLIPBOARD_BENCH_HUGE_CUT_BLOCKS=50000 SLATE_CLIPBOARD_BENCH_ISSUE_TARGETS=1 bun ./scripts/benchmarks/slate/5945-large-plaintext-paste.mjs
35.69ms, populated copy 39.81ms, populated middle
paste 285.79ms.cutTwoBlocksEditMs 552.21ms vs 150ms,
cutTwoBlocksMs 382.5ms vs 250ms.cd packages/slate-react && bun run test:vitest test/dom-strategy-and-scroll.test.tsx
37 tests passed.cd packages/slate-react && bun run test:vitest test/use-slate-history.test.tsx
3 tests passed.PLAYWRIGHT_RETRIES=0 bunx playwright test playwright/integration/examples/huge-document.test.ts --project=chromium --grep "virtualized|Huge Document"
6 Chromium tests passed.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
4 Chromium stress rows passed.node --check scripts/benchmarks/browser/react/huge-document-legacy-compare.mjs
bun lint:fix, bun lint, bun typecheck:root, bun check
bun check ran package/site/root typecheck plus Bun and Vitest
suites: 1157 Bun tests passed, 95 skipped; 25 slate-layout tests
passed; 39 slate-react Vitest files / 355 tests passed.Closeout truth:
Current pass: ralph-large-document-performance-virtualization-execution.
Current pass status: complete.
Lane status: done.
Next pass: none.
Next action: none. Keep #5945, #4056, and #5992 as Improves; track the
10000 selection-inclusive stress debt, 50000 cut threshold miss, and core
operation regression as follow-up lanes rather than hidden closure claims.