DEV Community

钟志敏
钟志敏

Posted on

Breaking the 100k Row Limit: Building High-Performance Dynamic Grids with VyncCanvasScroll in Vue 3

The Million-Row Challenge in Modern Web Apps
When dealing with massive enterprise datasets—like Amazon SKU multi-account performance metrics or high-frequency logistics tracking—standard DOM rendering breaks down. Even with standard Vue 3 virtual scrolling components, having tens of thousands of complex cells with custom tooltips can cause noticeable micro-stutters due to the sheer volume of DOM nodes.

To solve this, I've been experimenting with a hybrid architecture I call VyncCanvasScroll.

The core concept of VyncCanvasScroll is simple yet extremely powerful: Use the Vue 3 reactivity system to manage the viewport state, but completely bypass the DOM for the data grid, rendering everything directly onto a single HTML5 Canvas.

Why Standard Virtual Scrolling Fails at Scale
Traditional virtual scrolling only renders the DOM nodes visible in the current viewport. However:

CPU Overhead: As the user scrolls rapidly, the continuous destruction and recreation of DOM nodes trigger heavy browser Recalculate Style and Layout phases.

GC Spikes: The rapid creation of VNodes in Vue 3 under fast scrolling cycles triggers Garbage Collection spikes, leading to dropped frames.

The VyncCanvasScroll Core Implementation
By using a hybrid Canvas approach, we reduce the DOM count to exactly one element, regardless of whether you have 100 rows or 1,000,000 rows.

Here is a simplified structural layout of how the VyncCanvasScroll mechanism handles dynamic row heights and precise scrolling offsets in Vue 3:

TypeScript
// A sneak peek into the VyncCanvasScroll rendering engine core
import { ref, onMounted, watchEffect } from 'vue';

interface GridConfig {
rowHeight: number;
colWidth: number;
totalRows: number;
}

export function useVyncCanvasScroll(canvasRef: ref, config: GridConfig) {
const scrollTop = ref(0);
const scrollLeft = ref(0);

const renderGrid = (ctx: CanvasRenderingContext2D) => {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

// Calculate visible range based on unique offset positioning
const startRow = Math.floor(scrollTop.value / config.rowHeight);
const endRow = Math.min(config.totalRows, startRow + Math.ceil(ctx.canvas.height / config.rowHeight) + 1);

for (let i = startRow; i < endRow; i++) {
  const y = i * config.rowHeight - scrollTop.value;

  // Render Cell Borders & Backgrounds
  ctx.strokeStyle = '#e2e8f0';
  ctx.strokeRect(0, y, config.colWidth, config.rowHeight);

  // Render Text Content efficiently
  ctx.fillStyle = '#1e293b';
  ctx.font = '14px Inter, sans-serif';
  ctx.fillText(`Row Index Data: ${i}`, 10, y + config.rowHeight / 2 + 5);
}
Enter fullscreen mode Exit fullscreen mode

};

return { scrollTop, scrollLeft, renderGrid };
}
Key Optimization Tricks
Offscreen Canvas Buffering: To prevent screen tearing during high-speed scrolling, VyncCanvasScroll pre-renders the next predictable batch of cells on an OffscreenCanvas in a Web Worker, then blits the image data back to the primary canvas via requestAnimationFrame.

Debounced Reactivity: While Vue 3’s ref is fast, triggering updates on every single pixel scroll event is unnecessary. We decouple the browser scroll listener using high-precision hardware timestamps (performance.now()).

Drop a comment below if you want the full open-source template for this hybrid canvas grid setup! Let's push web performance boundaries together. 🚀

I am Tang Junting.

Top comments (0)