A 200-page document in a browser editor is a large tree of paragraphs, marks and images, and every keystroke asks the browser to lay out a part of it. Typing starts to lag, scrolling stutters, and the page takes seconds to open. The first instinct is to virtualise the list, rendering only the visible part as the reader scrolls. That works for a feed of independent items. For a document, where paragraphs flow into one another and page breaks depend on their heights, it creates problems that are hard to fix later.
Measure before you virtualise
Most slow editors are slow for one of three reasons: too many nodes in the DOM, a layout forced on every change, or a decoration pass that runs over the whole document. Each has a different fix. Open the browser’s performance panel, type in the middle of a long document, and find which of these dominates. Virtualising solves the first only. Applying a decoration only to the changed paragraph solves the third, and it is often the cheaper win.
Limit work to the changed paragraph
Spell check markers, comment highlights and search matches are decorations on top of the content. Recomputing all of them on every keystroke is a common cause of lag, and it is also unnecessary, because a keystroke changes one paragraph. Recompute decorations only for the range that changed, and keep the rest from the previous pass. The cost then grows with the size of the edit, not with the size of the document.
function updateDecorations(
prev: DecorationSet,
doc: ProseNode,
changed: { from: number; to: number },
) {
// Map the old decorations through the edit, then recompute only the paragraphs touched.
const mapped = prev.map(changedMapping, doc);
const $from = doc.resolve(changed.from);
const $to = doc.resolve(changed.to);
const start = $from.start($from.depth);
const end = $to.end($to.depth);
return mapped.remove(mapped.find(start, end)).add(doc, findSpellErrors(doc, start, end));
}The sample is illustrative: the helper names stand in for whatever your editor provides. The principle is what matters. Old decorations are carried forward through the edit, and only the paragraphs that changed are checked again.
Keep heights stable before you hide anything
Virtualisation replaces off-screen content with a placeholder of estimated height. When the estimate is wrong, the scrollbar jumps as the reader scrolls, and the position they were reading moves. Measure the real height of each block as it is rendered and keep the measurement, so later estimates improve. Images and tables make the estimate hardest, because their height is unknown until they load. Reserve their space from their stored dimensions, not from the loaded image.
Page breaks and virtualisation do not mix easily
A print layout that shows page boundaries needs the height of every block, because a break falls wherever the content reaches the bottom of a page. If only the visible blocks are rendered, the editor cannot know where the later page breaks fall without measuring them. Either keep page layout as a separate pass that runs off screen, or drop the on-screen page breaks in favour of a plain flowing view while editing, and show the true pages in print preview.
Search must reach content that is not rendered
A search that only looks at rendered paragraphs misses matches further down the document, and the count it shows is wrong. Search the document model, not the DOM. Highlight the match by scrolling the paragraph into view, which renders it on demand. Keep the match count as a number from the model, so it is accurate before the reader scrolls to any of them.
A short test list
- Open a 200-page document and type in the middle. The keystroke should appear within one frame.
- Scroll quickly from the top to the bottom. The scrollbar should not jump as blocks load.
- Search for a word that appears only near the end. The count should be correct before you scroll there.
- Insert a large image near the top and confirm the content below does not jump when it loads.
- Switch to print preview and confirm the page breaks match what the printer produces.
RichTextEditor is a perpetual-licence JavaScript editor — one purchase, self-hosted, no metered editor loads. Download the evaluation or see how it compares.