跪拜 Guibai
← All articles
Frontend · JavaScript · Architecture

How Online Documents Survive 10,000 Blocks: Flat Records, Virtual Scrolling, and One Shared Editor

By 尤水就下 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

A document with tens of thousands of blocks stays editable only because data, layout, and rendering are decoupled and loaded on demand. Any frontend that renders long lists or collaborative content can hit the same freeze points, and these are the mechanisms that prevent them.

Summary

Block architecture turns a document into a flat map of records with stable ids. A page keeps only its direct child ids; each block carries its own children, parent, type, and data. That flattening is what makes local updates, fine-grained collaboration, paginated loading, and content reuse possible. First-screen load sends structure and a few blocks first, marks the rest as pending, and replaces them as they scroll into view.

Rendering leans on a LayoutTree that stores every block's top and height. Blocks without a measured height get a default value, and when real height arrives the tree corrects that block plus everything after it, while compensating scrollTop so the current viewport block doesn't jump. Only viewport blocks and small buffers are mounted; everything else collapses into placeholders. Rich text is compressed by separating plain text from an attribute pool, and collaborative edits reduce to OT JSON's insert, retain, and delete operations.

New block types register model, view, layout, and common capabilities with a kernel instead of adding branches to a core renderer. The same record can appear in multiple positions, with recordId identifying stored data and blockId identifying a runtime instance. The design separates a structure tree from a layout tree. A suggested build order starts with three block types, then a store, then operations, history, and selection, before adding collaboration and virtual scrolling.

Takeaways
— A Page stores only its direct child ids; a container block's descendants live in that block's own children array.
— Flattening all blocks into a recordMap with stable ids means one edit updates one record instead of a deep nested tree.
— First-screen loading can send structure plus a few real blocks, mark the rest as pending, and replace pending blocks later.
— Blocks without a measured height get a default fallback height so LayoutTree can compute top positions by accumulating heights.
— When a block's real height differs, LayoutTree corrects that block's height and all later top values, and scrollTop is adjusted to keep the current viewport block stable.
— Virtual scrolling mounts only viewport blocks plus upper and lower buffers; unrendered ranges become placeholder elements.
— Rich text is persisted as plain text, an attribute application string, and a shared attribute pool, not per-character style objects.
— OT JSON decomposes document edits into three atomic operations: insert, retain, and delete.
— One Editor instance with shared content handles cross-block selection, copy/paste, and undo; per-block editors would need their own cross-block communication.
— New block types register model, view, layout, and common capabilities with the kernel, so the core renderer doesn't grow conditional branches.
— recordId identifies stored data across sessions; blockId identifies a runtime instance only for the current editor session.
— The suggested build order for a block editor: heading/paragraph/image blocks, blockMap plus children, a store, per-block subscription, operations/history/selection, then collaboration and virtual scrolling.
Conclusions

The "just ask AI" advice for OT JSON is practical only if you can verify the generated code; knowing the term is not the same as knowing the invariants that keep concurrent edits consistent.

Separating structure from layout is what lets a document reflow without rebuilding the whole tree; the two-tree model is the load-bearing decision behind stable scrolling.

The recordId/blockId split gives "reuse" a concrete meaning: data can be shared while each position keeps its own runtime identity, which is why sync blocks work.

The proposed build order suggests most editor complexity lives in cross-block concerns, not in individual block rendering; selection, clipboard, and undo all need a global view.

Concepts & terms
Block architecture
A document model where a page is composed of independent blocks, each with its own type, data, stable identity, and lifecycle, instead of one monolithic nested tree.
recordMap
A flat dictionary of block records keyed by id; hierarchy and order are expressed by children arrays on parent blocks, not by nesting the records themselves.
LayoutTree
A runtime structure that stores each block's top, left, height, and width so the renderer can compute what is visible and where to scroll.
OT JSON
An operational transformation approach for collaborative text editing that represents changes as atomic insert, retain, and delete operations.
recordId vs blockId
recordId is the stable identity of stored data across sessions; blockId is a runtime-only identity for a block instance in the current editor session.
StructureTree vs LayoutTree
StructureTree tracks parent-child relationships; LayoutTree tracks position and size for rendering. Keeping them separate lets layout update without rewriting structure.
Attribute pool
A rich-text compression scheme where plain text is stored separately from style ranges, and reusable styles live in a shared pool referenced by position.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗