Rendering
Rendering
Section titled “Rendering”@witqq/spreadsheet renders everything on Canvas 2D instead of creating DOM nodes for each cell. This eliminates the DOM node ceiling and provides consistent 60 FPS performance regardless of dataset size.
RenderPipeline
Section titled “RenderPipeline”The rendering system uses a composable layer architecture. Each layer is responsible for one visual concern and draws onto the canvas in order:
| Order | Layer | Description |
|---|---|---|
| 1 | BackgroundLayer | Fills cell backgrounds with alternating row colors |
| 2 | CellTextLayer | Renders cell text content with type-specific formatting |
| 3 | CellStatusLayer | Shows cell status indicators (changed, saving, saved, error) |
| 4 | EmptyStateLayer | Shows empty state placeholder when no data |
| 5 | GridLinesLayer | Draws horizontal and vertical grid lines (conditional) |
| 6 | HeaderLayer | Renders column headers with sort/filter indicators |
| 7 | RowNumberLayer | Draws row numbers on the left side (conditional) |
| 8 | SelectionOverlayLayer | Draws selection highlight and active cell border |
Plugin layers (added via plugins, not part of core):
- ConditionalFormatLayer — applies conditional formatting (gradients, data bars, icons)
- RemoteCursorLayer — renders remote user cursors in collaboration mode
Each layer implements the RenderLayer interface:
interface RenderLayer { render(rc: RenderContext): void; measureHeights?(rc: RenderContext): Map<number, number>;}
interface RenderContext { ctx: CanvasRenderingContext2D; geometry: GridGeometry; theme: SpreadsheetTheme; canvasWidth: number; canvasHeight: number; viewport: ViewportRange; scrollX: number; scrollY: number; renderMode: RenderMode; paneRegion: PaneRegion; mergeManager?: MergeManager;}DirtyTracker
Section titled “DirtyTracker”Not every frame requires a full repaint. The DirtyTracker identifies what changed and triggers the minimum necessary re-render:
| Dirty Type | Trigger | What Re-renders |
|---|---|---|
full | Theme change, resize, data reload | Entire canvas |
viewport-change | Scroll, frozen pane toggle | Visible region only |
cell-update | Cell edit, status change | Single cell clip region |
For cell-update, the renderer uses ctx.save(), ctx.clip() to restrict drawing to the affected cell rectangle, then runs only the relevant layers.
RenderScheduler
Section titled “RenderScheduler”Multiple operations in a single frame (e.g., a paste affecting 50 cells) would normally trigger 50 render calls. The RenderScheduler coalesces these into a single requestAnimationFrame:
Frame N: setCell(0,0) → setCell(0,1) → setCell(0,2) → ... ↓ ↓ ↓ markDirty() markDirty() markDirty() (coalesced) ↓Frame N+1: ONE render() call with all dirty regions mergedViewportManager
Section titled “ViewportManager”The ViewportManager calculates which rows and columns are currently visible, plus a buffer zone for smooth scrolling:
interface ViewportRange { readonly startRow: number; // First visible row (inclusive, with buffer) readonly endRow: number; // Last visible row (inclusive, with buffer) readonly startCol: number; // First visible column (inclusive, with buffer) readonly endCol: number; // Last visible column (inclusive, with buffer) readonly visibleRowCount: number; // Visible rows including buffer readonly visibleColCount: number; // Visible columns including buffer}Only cells within the viewport (plus buffer) are rendered. For a 100K-row dataset, typically only 30-50 rows are drawn per frame.
Scroll Rendering
Section titled “Scroll Rendering”The RenderMode type supports 'full' | 'light' | 'placeholder' modes. Currently the mode is always 'full' — all cells are rendered with complete fidelity. The type union exists for future optimization where fast scrolling could switch to simplified rendering.
TextMeasureCache
Section titled “TextMeasureCache”Text measurement (ctx.measureText()) is expensive. The TextMeasureCache stores results in a Map keyed by font+text:
// Cache hit rate is typically >95% for real spreadsheetsconst width = textMeasureCache.measureText(ctx, 'Hello World', '14px Inter');- Capacity: 10,000 entries with LRU eviction
- Text truncation: Uses binary search to find the longest substring that fits in a cell width, then appends
"…"
Single-Canvas Architecture
Section titled “Single-Canvas Architecture”@witqq/spreadsheet renders all layers on a single canvas element. The CanvasManager applies a comprehensive CSS reset (margin, padding, border, outline, box-sizing, etc.) to prevent any framework or parent CSS from displacing the canvas. It also handles DPR (Device Pixel Ratio) scaling and detects browser zoom changes via matchMedia, automatically recalibrating canvas resolution.
Custom Render Layers
Section titled “Custom Render Layers”You can add custom layers to the pipeline or reorder existing ones:
engine.addRenderLayer(myCustomLayer);engine.insertRenderLayerBefore(myCustomLayer, selectionLayer);engine.removeRenderLayer(emptyStateLayer);| Method | Description |
|---|---|
addRenderLayer(layer) | Append layer at end of pipeline |
insertRenderLayerBefore(layer, beforeLayer) | Insert before an existing layer |
removeRenderLayer(layer) | Remove a layer |
Frozen Pane Rendering
Section titled “Frozen Pane Rendering”When frozen rows or columns are enabled, the render pipeline splits rendering into 4 regions:
Frozen regions are cached as ImageData and only re-rendered when their content changes, not on every scroll frame.