How did they create a performant custom data grid?
How did they create such a performant data grid with custom column types?
Perfect! Now I have enough information to provide a comprehensive answer. Let me write it up:
Based on my exploration of the Quadratic source code, here's how they created such a performant data grid with custom column types:
##Architecture Overview
Quadratic uses a WebGL-based rendering system with PixiJS as the foundation. This is a game-engine approach to spreadsheet rendering, which provides exceptional performance for pan, zoom, and real-time updates.
###Key Performance Strategies:
##1. GPU-Accelerated Rendering (WebGL via PixiJS)
The entire grid is rendered to GPU rather than DOM. From their documentation:
"The Quadratic Grid is built on WebGL using PixiJS. This allows us to render a high-performance grid with all your data, where you can quickly pan and zoom. By basically using a game engine to render the grid, this gives us a high level of control over what is drawn to the grid and our render pipeline."
##2. Spatial Hashing for Cell Batching
Text rendering is optimized through spatial hashing. From the CELL_LABEL.md documentation:
- The grid is divided into 15x30 cell hash buckets (
sheetHashWidth=15, sheetHashHeight=30) - All text within a hash region is batched together
- Each
CellsTextHashmanages a viewport rectangle, reducing rendering overhead through culling
This dramatically reduces the number of geometries sent to the GPU.
##3. Multi-Threaded Rendering Pipeline
Text rendering is offloaded to a Web Worker (renderWebWorker):
// From renderWebWorker.ts
class RenderWebWorker {
private worker?: Worker;
// Handles all text rendering in a separate thread
// Returns LabelMeshEntries that contain GPU buffers
}
This keeps the main thread responsive while expensive rendering calculations happen in parallel.
##4. Efficient GPU Mesh Management
Text is rendered using MSDF (Multi-channel Signed Distance Field) shaders with specialized shader code:
cellLabelShader.ts- For black text (no color per vertex)cellLabelShaderTint.ts- For colored text (requires color data per vertex)
By separating colored and black text, they reduce GPU buffer sizes.
##5. Lazy Loading and Viewport Culling
The rendering system only renders cells visible in the viewport:
// From Update.ts
updateViewport() {
const viewportChanged = pixiApp.viewport.updateViewport();
const contentDirty = content.update(viewportChanged);
// Only re-render if viewport OR content changed
if (pixiApp.viewport.dirty || contentDirty) {
pixiApp.renderer.render(pixiApp.stage);
}
}
##6. Custom Column Types (Tables)
Tables with custom column types are rendered using a modular system:
// From Tables.ts
export class Tables extends Container<Table> {
// Each table is a Container with:
private activeTables: Table[] = [];
private columnTables: Table[] = [];
hoverTableHeaders: Container; // Sticky headers
}
// From Table.ts
export class Table extends Container {
header: TableHeader; // Column headers with custom rendering
outline: TableOutline; // Table borders
codeCell: JsRenderCodeCell; // Data with column metadata
}
// From TableColumnHeader.ts
export class TableColumnHeader extends Container {
sortButton?: Graphics; // Interactive sort UI
columnName: BitmapText; // Bitmap font for performance
// Handles column-specific rendering and events
}
##7. Reactive Event System
Dirty flag system ensures only affected areas re-render:
// Changes trigger events that mark specific regions dirty
events.emit('setDirty', {
gridLines: true,
headings: true,
cursor: true,
cellHighlights: true
});
##8. Smart Text Clipping
Column headers automatically clip text when it overflows:
private clipName = (name: string, width: number) => {
while (clippedName.length > 0 &&
this.columnName.width + SORT_BUTTON_RADIUS * 2 > width) {
clippedName = clippedName.slice(0, -1);
this.columnName.text = clippedName + '…';
}
};
##9. Hierarchical Container Structure
The rendering hierarchy is efficient:
Stage
├── Background
├── CellsSheets (spatial data)
├── GridLines
├── Tables
│ ├── Table
│ │ ├── TableHeader
│ │ ├── TableColumnHeaders (with sort buttons)
│ │ └── TableOutline
│ └── (more tables...)
└── UI Elements (cursor, selections, etc.)
##10. Decoupled Logic and Rendering
- Core logic: Rust-based
quadratic-core(compiled to WASM) - Client rendering: TypeScript/PixiJS
- Communication: Message passing between Web Workers
This allows heavy computation (formulas, data processing) to happen in WASM while rendering stays fast.
##Summary
The performance comes from:
- GPU acceleration instead of DOM rendering
- Spatial hashing for smart batching
- Web Workers for parallel processing
- Viewport culling to only render what's visible
- Bitmap fonts instead of DOM text
- MSDF shaders for quality text rendering
- Modular component system for custom column types
- Lazy updates with dirty flags
This combination allows Quadratic to handle millions of cells while maintaining 60 FPS performance and supporting custom column types with interactive features like sorting and renaming.
END OF POST