How did they create a performant custom data grid?

qa
32ff45cMerge pull request #3740 from quadratichq/ignore-second-code
This post might have stale content, as qa is 203 commits ahead.

How did they create such a performant data grid with custom column types?

Avatar of Claude Haiku 4.5Claude Haiku 4.5commented

I'll help you explore how the Quadratic project created their performant data grid. Let me start by examining the repository structure.

Let me look at the client-side code, which is likely where the data grid is implemented:

Let me explore the grid components more closely, particularly the gridGL and grid folders:

Now let me examine the core files that handle cell rendering and column types. Let me look at the cells types and tables implementations:

Let me search for information about column types and how they're implemented:

Let me look at the TableColumnHeader to understand how custom column types work:

Now let me search for how data table columns are handled and their types:

Let me look at the type definitions to understand the data structure:

Let me search for how the rendering pipeline works with pixi.js:

Let me check the PixiApp and Content to understand the rendering architecture:

Now let me look at how cells are rendered, particularly the labels and text rendering:

Let me search for how rendering is optimized - particularly looking at the RenderWebWorker:

Perfect! Now let me look at the data table column type implementation:

Let me search for data table column type handling in the core:

Let me search for information about data tables more broadly:

Let me search in the quadratic-core more specifically:

Let me search for where DataTable is defined:

Let me look at the core types more broadly to understand the overall architecture:

Let me check the docs:

Perfect! Now let me look more at the rendering architecture and how table columns are handled. Let me check the Tables.ts file more completely:

Now let me create a comprehensive answer by looking at one more key file - how the rendering system handles updates:

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 CellsTextHash manages 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:

  1. GPU acceleration instead of DOM rendering
  2. Spatial hashing for smart batching
  3. Web Workers for parallel processing
  4. Viewport culling to only render what's visible
  5. Bitmap fonts instead of DOM text
  6. MSDF shaders for quality text rendering
  7. Modular component system for custom column types
  8. 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

Log in to add a comment
Claude Haiku 4.5
How did they create a performant custom data grid? — quadratichq/quadratic