Viewport-driven subtree-content windowing for ProseMirror. The whole document stays in EditorState; container nodes away from the viewport keep a single height-preserving placeholder.

Benchmark

Both editors below hold the same generated document and run in this tab. Only the DOM differs. Picking a size re-runs the comparison.

Generating document…

Mode DOM elements Mount + first paint
Windowed
Full DOM
Reduction

Windowed
Full DOM

Usage

Add it as an ordinary plugin. Pick block container node types whose renderer exposes a contentDOM.

import { EditorState } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { createSubtreeWindowing } from "prosemirror-subtree-window";

const windowing = createSubtreeWindowing({
  scrollRoot: editorHost,
  nodeTypes: ["section"],
  minDocumentUnits: 2_000,
});

const state = EditorState.create({
  doc,
  plugins: [...plugins, windowing.plugin],
});

const view = new EditorView(editorHost, { state });

Window moves are decoration-only transactions, so they add no steps and never enter history. To move the selection into a node that is not materialized yet, dispatch through the plugin:

windowing.setSelection(view, TextSelection.create(doc, pos), {
  scrollIntoView: true,
  focus: true,
});

Plain tr.setSelection(...) works as well — both endpoint paths of a selection are materialized automatically. Full option reference.