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);
}
};
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)