Back to Plate

Slate v2 TanStack Virtual iOS/perf refresh ralplan

docs/plans/2026-05-23-slate-v2-tanstack-virtual-ios-perf-ralplan.md

53.0.625.8 KB
Original Source

Slate v2 TanStack Virtual iOS/perf refresh ralplan

Current Verdict

Upgrade TanStack Virtual. Do not change Slate's public API.

The 2026-05-19 TanStack Virtual release is not "just inner" for Slate because our virtualized mode currently bypasses TanStack for one layout-backed scroll branch. The right plan is:

  1. bump .tmp/slate-v2 from @tanstack/[email protected] / @tanstack/[email protected] to @tanstack/[email protected] / @tanstack/[email protected];
  2. keep domStrategy={{ type: 'virtualized', threshold, overscan, estimatedBlockSize }} as the public Slate-shaped API;
  3. keep TanStack raw options private;
  4. route internal layout-backed scrollToTopLevelIndex through TanStack's scrollToOffset / scrollToIndex path where practical instead of direct rootElement.scrollTo;
  5. do not override shouldAdjustScrollPositionOnItemSizeChange;
  6. do not expose takeSnapshot() publicly yet.

Blunt take: the public usage is already mostly right. The internal scroll bypass is the only thing I would change now.

Current score after pass 3: 0.92. Target score: >= 0.92, with no dimension below 0.85.

This lane is done and ready for Ralph execution.

Intent

Refresh Slate v2 virtualization strategy against the latest TanStack Virtual performance/iOS work and decide whether Slate usage should change.

Outcome

Ralph should get a narrow implementation handoff:

  • update the TanStack Virtual dependency;
  • keep Slate public API unchanged;
  • route internal virtualized scroll writes through TanStack where that preserves Slate layout offsets;
  • add browser/proof rows for backward scroll and iOS/momentum behavior;
  • keep virtualized mode experimental.

Source Grounding

External:

  • TanStack blog https://tanstack.com/blog/tanstack-virtual-perf-and-ios, published 2026-05-19 and read 2026-05-23.
  • NPM current metadata read on 2026-05-23: @tanstack/[email protected] depends on @tanstack/[email protected].
  • TanStack Virtual latest Virtualizer API docs read on 2026-05-23.

Live .tmp/slate-v2:

  • packages/slate-react/package.json:18-24 declares @tanstack/react-virtual as a slate-react dependency with range ^3.13.24.
  • bun.lock:563-565 resolves @tanstack/[email protected] and @tanstack/[email protected], so the latest core perf/iOS fixes are not installed.
  • packages/slate-react/src/dom-strategy/use-virtualized-root-plan.ts:95-115 retains selected/promoted indexes through a custom rangeExtractor.
  • packages/slate-react/src/dom-strategy/use-virtualized-root-plan.ts:242-268 uses estimateSize, runtime-id getItemKey, getScrollElement, initialRect, overscan, and rangeExtractor.
  • packages/slate-react/src/dom-strategy/use-virtualized-root-plan.ts:345-367 directly calls rootElement.scrollTo(...) for layout-backed targets before falling back to virtualizer.scrollToIndex(...).
  • docs/libraries/slate-react/experimental-virtualized-rendering.md:20-44 keeps TanStack Virtual internal and does not expose raw virtualizer options.
  • docs/libraries/slate-react/experimental-virtualized-rendering.md:46-61 explicitly labels native find, screen-reader, IME, and mobile limitations.

Decision Brief

Principles:

  1. TanStack owns viewport math, not editor semantics.
  2. Slate owns DOM coverage, materialization, selection, copy/paste, IME, mobile, browser-find, a11y, and metrics.
  3. The default editor path stays native/DOM-present.
  4. virtualized stays explicit and experimental.
  5. Public Slate API stays editor-shaped, not TanStack-shaped.

Top drivers:

  • latest upstream iOS and backward-scroll fixes;
  • extreme document performance;
  • native contenteditable behavior under missing DOM;
  • public API cleanliness.
OptionVerdictReason
Treat the release as purely internal and only bump the lockfilereviseMost gains are internal, but Slate currently bypasses TanStack for one programmatic scroll branch.
Expose latest TanStack options on EditablerejectLeaks a list virtualizer into Slate API and pushes missing-DOM policy onto users.
Keep current Slate API and route internal scroll writes through TanStackchooseGets upstream iOS/backward-scroll behavior without changing userland API.
Add public snapshot/cache API nowrejecttakeSnapshot() is useful upstream, but Slate needs a proven remount-jump problem before public API.
Make virtualized mode default after the perf releaserejectMissing DOM still degrades native find, a11y, IME/mobile, and broad selection behavior.

TanStack Update Implications

Upstream changeSlate implicationVerdict
Typed-array single-lane hot pathKeep Slate top-level block virtualization single-lane; do not add lanes.keep
Lazy VirtualItem materializationCurrent getVirtualItems() use benefits after upgrade.inner
Resize storm/cache-version fixDynamic block measurement benefits after upgrade.inner
iOS momentum/elastic scroll write deferralAvoid direct scroll writes in virtualized adapter where TanStack can own the write.change internal
Backward-scroll adjustment defaultDo not override shouldAdjustScrollPositionOnItemSizeChange unless a measured editor-specific regression appears.keep upstream default
takeSnapshot() restorationConsider internal cache only if root/remount route proves jumpy.defer

Public API Target

Keep:

tsx
<Editable
  domStrategy={{
    estimatedBlockSize: 32,
    overscan: 4,
    threshold: 25_000,
    type: 'virtualized',
  }}
  style={{ height: 480, overflowY: 'auto' }}
/>

Do not expose:

  • getScrollElement
  • measureElement
  • rangeExtractor
  • shouldAdjustScrollPositionOnItemSizeChange
  • takeSnapshot
  • initialMeasurementsCache
  • lanes
  • raw TanStack Virtualizer instance

Internal Runtime Target

Ralph implementation target:

  • run the package manager update in .tmp/slate-v2 so bun.lock resolves @tanstack/[email protected] and @tanstack/[email protected];
  • change layout-backed scrollToTopLevelIndex to call virtualizer.scrollToOffset(Math.max(0, top)) or an equivalent TanStack-owned scroll path when it preserves Slate layout offsets;
  • keep the fallback virtualizer.scrollToIndex(index, { align });
  • memoize getItemKey / getScrollElement if the new package makes option identity churn visible in tests or profiles;
  • keep selected/promoted index retention as Slate-owned rangeExtractor logic;
  • do not add custom shouldAdjustScrollPositionOnItemSizeChange by default;
  • keep takeSnapshot() private and unshipped unless a remount restoration test proves the need.

Performance Lens

  • applicability: applied
  • Vercel rules used: event/listener and subscription minimalism by existing Slate virtualized adapter shape
  • extra rules used: repeated-unit budget, degradation contract, editor-native-behavior proof
  • repeated unit: top-level Slate runtime block
  • cohorts: normal DOM-present, large staged, pathological virtualized
  • budget: virtualized mode must reduce mounted top-level blocks without global selection/copy/paste semantic loss
  • React/runtime primitive: TanStack Virtual remains range engine; React is not editor semantics
  • degradation contract: native find and full screen-reader traversal remain limited until mounted
  • plan delta: upgrade dependency and remove direct scroll-write bypass

Regression Proof Required From .tmp/slate-v2

Focused commands for Ralph:

bash
bun update @tanstack/react-virtual
bun test ./packages/slate-react/test/dom-strategy-and-scroll.tsx
bun --filter ./packages/slate-react typecheck
PLAYWRIGHT_RETRIES=0 bun run playwright playwright/integration/examples/huge-document.test.ts --project=chromium --workers=1 --grep virtualized
PLAYWRIGHT_RETRIES=0 bun run playwright playwright/integration/examples/pagination.test.ts --project=chromium --workers=1 --grep virtualized
bun lint:fix

Add or preserve proof rows:

  • virtualized backward-scroll with dynamic heights does not jump upward;
  • layout-backed scrollToTopLevelIndex keeps exact Slate layout offsets;
  • browser/iOS lane is semantic until real iOS proof exists;
  • virtualized mode stays explicit and degraded;
  • native find/a11y limits stay documented.

Issue Accounting

Pass 2 classification:

  • New fixed claims: 0.
  • New improved claims: 0.
  • #790: related performance/virtualization proof-route backlog only.
  • #5826, #5538, #4995, #5088, #5473: related scroll/focus rows only. Upstream iOS momentum scroll deferral supports the internal scroll-routing implementation route, but exact Slate repro closure still needs targeted proof.
  • #5391, #5095, #4751, #4354, #3760: related/non-claimed mobile rows only. TanStack iOS scroll handling is not proof of Slate iOS selection, IME, spellcheck, or native toolbar correctness.

Synced artifacts:

  • docs/slate-issues/gitcrawl-v2-sync-ledger.md: added 2026-05-23 TanStack Virtual iOS/perf sync notes and corrected stale virtualized adapter paths.
  • docs/slate-v2/ledgers/issue-coverage-matrix.md: updated #790 policy row to reference latest TanStack Virtual as internal proof-route work, not a closure claim.
  • docs/slate-v2/ledgers/fork-issue-dossier.md: added a TanStack Virtual iOS/perf refresh section with issue classifications and zero new claims.
  • docs/slate-v2/references/pr-description.md: unchanged; this pass changes no public API shape, no exact fixed issue claim, and no release-ready proof row. Ralph implementation may update it after package/browser proof.

Pass 3 must run maintainer/high-risk/performance closure pressure before this can become a Ralph-ready handoff.

Confidence Scorecard

DimensionWeightScoreEvidenceReason
React 19.2 runtime performance0.200.90Live virtualized adapter, TanStack latest perf/iOS update, npm metadata.Internal adapter shape is strong; dependency is one patch behind.
Slate-close unopinionated DX0.200.92Existing docs hide TanStack raw options.Public API remains editor-shaped.
Plate and slate-yjs migration backbone0.150.86DOM coverage/degradation boundary remains Slate-owned.Good backbone, but issue/collab pass still pending.
Regression-proof testing strategy0.200.84Existing package/browser virtualized rows plus new required backward-scroll/iOS rows.Below floor until proof rows are added or linked.
Research evidence completeness0.150.88Research source refreshed with latest TanStack release and live Slate source.Good enough for pass 1; issue ledger still pending.
shadcn-style composability and hook/component minimalism0.100.90Public API keeps minimal domStrategy options.No raw virtualizer escape hatch.

Weighted total after pass 1: 0.88.

Still pending because regression-proof testing is below 0.85, issue sync is not complete, and closure gates have not run.

Score After Pass 2

Total rises from 0.88 to 0.89.

DimensionPreviousCurrentReason
React 19.2 runtime performance0.900.90No new implementation proof; dependency/scroll route target unchanged.
Slate-close unopinionated DX0.920.92Public API remains unchanged and TanStack raw options stay private.
Plate and slate-yjs migration backbone0.860.88DOM coverage/degradation issue rows are now synced with zero new claims.
Regression-proof testing strategy0.840.86Issue sync now names exact scroll, mobile, and benchmark proof routes.
Research evidence completeness0.880.90Durable issue ledgers now reflect the latest TanStack research update.
shadcn-style composability and hook/component minimalism0.900.90No change.

Weighted total: 0.89.

Still pending because the maintainer/high-risk pass and closure gates are open.

Pass 3 - Maintainer / High-Risk / Performance Closure Pressure

Steelman, high-risk deliberate, and performance pressure pass applied.

Maintainer Objection Ledger

DecisionStrongest fair objectionSteelman antithesisTradeoff tensionAnswerVerdict
Upgrade TanStack Virtual to the latest patch release"Why touch a dependency in an editor rewrite when virtualized mode is experimental?"Leave dependency alone until virtualized mode is release-ready.Any dependency bump can introduce scroll or measurement behavior changes.Slate already ships the dependency in slate-react; the latest release fixes the exact class of dynamic-height, backward-scroll, and iOS scroll-write behavior our experimental mode exercises. Keep proof scoped to virtualized rows.keep
Keep Slate public API unchanged"If TanStack has new knobs like shouldAdjustScrollPositionOnItemSizeChange and takeSnapshot, shouldn't advanced users get them?"Expose a virtualizerOptions escape hatch and let users own it.Hiding options may slow edge-case experiments.Raw virtualizer options make users responsible for missing-DOM editor semantics. Slate should own the policy and expose editor-shaped options only.keep
Route layout-backed scroll through TanStack"Slate layout offsets are more exact than virtualizer estimates; direct scroll is simpler and deterministic."Keep the direct rootElement.scrollTo branch for layout-backed offsets.Routing through TanStack adds adapter coupling and must preserve exact layout offsets.The chosen implementation can still pass the exact offset to scrollToOffset. The point is not to give up Slate layout; it is to let TanStack own the scroll write path so iOS momentum deferral applies.revise internal
Do not override backward-scroll adjustment policy"Editor blocks are dynamic; maybe Slate needs its own scroll correction logic."Add a Slate-specific shouldAdjustScrollPositionOnItemSizeChange callback.Overriding may fight upstream fixes and create custom behavior to maintain.Start with upstream default because the release specifically fixes backward-scroll dynamic-height jank. Add override only after a measured Slate regression.keep
Do not expose takeSnapshot() publicly"Snapshot restoration sounds useful for remounting giant editors."Add public snapshot/cache options now while adopting the release.Public cache APIs create durability and invalidation promises.Keep it internal until we prove a root remount/restoration jump. No user-facing API without a failing test.keep

High-Risk Trigger

This pass touches a browser-sensitive runtime dependency and virtualized scroll behavior. The blast radius is limited to experimental virtualized mode, but the failure modes are visible: jumpy scroll, wrong target materialization, broken copy/selection over missing DOM, and false iOS confidence.

Blast Radius

AreaRiskGuardrail
DependencyTanStack patch changes measurement or scroll semantics.Focused package tests plus huge-document and pagination virtualized browser rows.
Scroll routingscrollToOffset path may not preserve exact Slate layout offset.Add/assert layout-backed scroll target keeps expected offset.
Dynamic heightBackward scroll may jump with measured block changes.Add virtualized backward-scroll dynamic-height row.
iOSUpstream iOS scrollTop fix may be mistaken for Slate iOS editing proof.Keep iOS issue rows related only; require raw iOS/device proof for selection/IME claims.
Public APIUsers may ask for raw TanStack knobs.Keep docs explicit: TanStack is internal; Slate owns missing-DOM policy.
Accessibility/native behaviorMissing DOM still breaks native find/full screen-reader traversal.Keep virtualized mode explicit, degraded, and experimental.

Three-Scenario Pre-Mortem

  1. Dependency upgrade looks green but scroll jumps in a real editor.

    Cause: package tests only prove mounted ranges, not dynamic-height backward scroll after measurement. Guardrail: browser row for backward scroll with measured variable-height blocks.

  2. iOS blog headline creates false confidence.

    Cause: TanStack fixed scrollTop writes during momentum scroll, not Slate's iOS selection handles, IME, spellcheck, or toolbar behavior. Guardrail: issue ledger keeps iOS rows related/non-claimed and Ralph proof names iOS as semantic until raw-device proof exists.

  3. Slate leaks TanStack API to solve one edge case.

    Cause: a user asks for shouldAdjustScrollPositionOnItemSizeChange or takeSnapshot directly. Guardrail: keep raw options private; add internal targeted behavior only after a failing Slate test proves need.

Final Proof Matrix For Ralph

Proof rowCommand / ownerClosure meaning
Dependency resolves latest corebun update @tanstack/react-virtual; inspect bun.lock@tanstack/[email protected] and @tanstack/[email protected] installed.
Package virtualized contractsbun test ./packages/slate-react/test/dom-strategy-and-scroll.tsxExisting DOM strategy metrics, materialization, and scroll contracts still pass.
Type/API surfacebun --filter ./packages/slate-react typecheckNo public API leak or type regression.
Huge-document virtualized browser rowPLAYWRIGHT_RETRIES=0 bun run playwright playwright/integration/examples/huge-document.test.ts --project=chromium --workers=1 --grep virtualizedPublic example still exposes explicit virtualized mode and metrics.
Pagination virtualized browser rowPLAYWRIGHT_RETRIES=0 bun run playwright playwright/integration/examples/pagination.test.ts --project=chromium --workers=1 --grep virtualizedLayout-backed virtualized offsets still behave after scroll-routing change.
Backward-scroll dynamic-height rowNew or existing focused browser/unit rowDynamic measurement does not jump upward while scrolling backward.
iOS semanticsOwner: future raw-device proofNo iOS issue closure until raw-device artifacts exist.
Lintbun lint:fixFormatting and lint cleanup after dependency/code change.

Performance Verdict

The best architecture is unchanged:

  • normal editors use DOM-present/staged rendering;
  • pathological documents may opt into explicit virtualized;
  • TanStack Virtual owns visible range, measurement, overscan, and scroll writes;
  • Slate owns DOM coverage, materialization, model-backed copy/paste, selection, IME/mobile policy, browser-find/a11y limits, and RUM metrics.

The only implementation revision is internal: avoid direct scroll writes in the virtualized adapter when TanStack can perform the same offset write.

Score After Pass 3

Total rises from 0.89 to 0.92.

DimensionPreviousCurrentReason
React 19.2 runtime performance0.900.92High-risk proof now ties dependency upgrade, scroll routing, and dynamic-height rows to concrete gates.
Slate-close unopinionated DX0.920.94Public API stays Slate-shaped and raw TanStack options stay private.
Plate and slate-yjs migration backbone0.880.90DOM coverage and degraded-mode boundaries remain Slate-owned; no product wrapper or collab claim is added.
Regression-proof testing strategy0.860.92Proof matrix now names dependency, package, browser, dynamic-height, iOS, and lint gates.
Research evidence completeness0.900.92Latest TanStack, live Slate source, research note, and issue ledgers are all synced.
shadcn-style composability and hook/component minimalism0.900.91Minimal editor-shaped config remains the target.

Weighted total: 0.92.

The score threshold is met, but completion is still pending. Closure is the only remaining pass, and it must run separately.

Pass State

PassStatusEvidence addedPlan deltaOpen issuesNext owner
1. Current-state/latest TanStack readcompleteTanStack 2026-05-19 blog, latest npm metadata, current slate-react package/lockfile, current virtualized adapter, and current docs read.Added verdict: upgrade dependency, keep public API, change internal scroll path, defer public snapshot API.Issue sync, maintainer/high-risk, and closure remain.Slate Ralplan
2. Issue-ledger and current sync passcompleteLive open ledger, manual sync ledger, issue coverage matrix, fork issue dossier, clusters, package impact matrix, requirements, and benchmark candidate map read.Synced #790 as related proof-route backlog; kept scroll/iOS rows related only; added latest TanStack sync notes; corrected stale virtualized adapter paths.Need maintainer/high-risk pass and final score.Slate Ralplan
3. Maintainer/high-risk/performance closure passcompleteMaintainer objection ledger, high-risk trigger/blast radius, three-scenario pre-mortem, final proof matrix, and performance verdict added.Kept public API unchanged, kept upgrade/internal scroll-routing target, raised score to threshold, and kept closure separate.Need closure score/final gates.Slate Ralplan
4. Closure score and final gatescompletePass rows, score threshold, issue sync, PR-reference decision, allowed edit scope, completion state, and continuation state audited.Marked lane done and added Done Handoff.None for Slate Ralplan; implementation belongs to Ralph.Ralph

Pass 4 - Closure Score And Final Gates

Closure pass applied.

Closure Audit

GateResultEvidence
Pass schedulepassPass rows 1-3 were complete before this closure pass; pass 4 is now complete.
Current passpasscurrent_pass is closure-score-and-final-gates; current_pass_status is complete.
Score thresholdpassFinal score is 0.92, meeting the >= 0.92 threshold.
Dimension floorpassLowest final dimension is 0.90, above the 0.85 floor.
Research syncpassTanStack research source and research log were refreshed against 2026-05-23 evidence.
Issue syncpass#790, scroll/focus rows, and iOS/mobile rows were classified with zero new fixed/improved claims.
PR-reference decisionpassPR reference intentionally unchanged because this plan changes no public API shape, no exact fixed claim, and no release-ready proof row before Ralph execution.
Maintainer/high-risk passpassObjection ledger, blast radius, pre-mortem, and final proof matrix are complete.
Allowed edit scopepassThis Slate Ralplan edited only planning, research, issue-ledger/reference, and scoped .tmp state artifacts. No .tmp/slate-v2 implementation files were edited.
Ralph handoffpassImplementation scope and .tmp/slate-v2 verification commands are named.

Done Handoff

  • Public API: keep domStrategy={{ type: 'virtualized', threshold, overscan, estimatedBlockSize }}.
  • Public API: do not expose getScrollElement, measureElement, rangeExtractor, shouldAdjustScrollPositionOnItemSizeChange, takeSnapshot, initialMeasurementsCache, lanes, or raw TanStack Virtualizer.
  • Dependency: upgrade .tmp/slate-v2 to @tanstack/[email protected], which resolves @tanstack/[email protected].
  • Internal runtime: route layout-backed scrollToTopLevelIndex through TanStack's scroll path where practical instead of direct rootElement.scrollTo.
  • Internal runtime: keep Slate layout offsets authoritative; pass exact offsets through scrollToOffset when using TanStack for the scroll write.
  • Internal runtime: keep selected/promoted index retention as Slate-owned rangeExtractor logic.
  • Internal runtime: do not override shouldAdjustScrollPositionOnItemSizeChange by default.
  • Internal runtime: keep takeSnapshot() private and unshipped unless a remount/restoration test proves the need.
  • Rendering strategy: keep virtualized mode explicit, degraded, and experimental.
  • Native behavior: native find and full screen-reader traversal remain limited until mounted.
  • iOS: TanStack's iOS scrollTop deferral is useful for scroll writes, but it is not Slate iOS selection/IME/spellcheck proof.
  • Issue accounting: #790 remains related proof-route backlog.
  • Issue accounting: #5826, #5538, #4995, #5088, and #5473 remain related scroll/focus rows only.
  • Issue accounting: #5391, #5095, #4751, #4354, and #3760 remain related/non-claimed mobile rows only.
  • Claim counts: new fixed claims 0; new improved claims 0.
  • Ralph verification:
bash
bun update @tanstack/react-virtual
bun test ./packages/slate-react/test/dom-strategy-and-scroll.tsx
bun --filter ./packages/slate-react typecheck
PLAYWRIGHT_RETRIES=0 bun run playwright playwright/integration/examples/huge-document.test.ts --project=chromium --workers=1 --grep virtualized
PLAYWRIGHT_RETRIES=0 bun run playwright playwright/integration/examples/pagination.test.ts --project=chromium --workers=1 --grep virtualized
bun lint:fix
  • Ralph proof additions: backward-scroll dynamic-height row and layout-backed virtualized scroll offset row.

Ralph Execution Log

  • 2026-05-23 08:32 CEST: Ralph execution started. Completion state reset to pending for runtime id 019e5374-b6ff-78f3-848c-dda660d40b64; active implementation owner is .tmp/slate-v2.
  • 2026-05-23 08:40 CEST: Ralph execution complete. .tmp/slate-v2 updated slate-react to @tanstack/[email protected] / @tanstack/[email protected], routed layout-backed virtualized scroll through virtualizer.scrollToOffset, added exact-offset and dynamic-height backward-scroll proof rows, and added a slate-react patch changeset. Verification passed: bun install --frozen-lockfile; package Vitest dom-strategy-and-scroll.test.tsx with 37 tests; bun --filter ./packages/slate-react typecheck; huge-document virtualized Playwright grep with 2 tests; pagination virtualized Playwright grep with 1 test; and final bun lint:fix. The raw plan command bun test ./packages/slate-react/test/dom-strategy-and-scroll.tsx was not the right runner for this Vitest-backed file.