Introduction
If you've ever built a large data-intensive application, you've probably heard this advice:
"Use virtualization. It solves performance."
For many applications, that's absolutely true.
Libraries like react-window, TanStack Virtual, and react-virtualized have transformed how we render long lists by ensuring that only the visible rows exist in the DOM.
So here's the obvious question:
If virtualization already renders only 30–50 rows, why would anyone build a table using Canvas?
It's a fair question.
In fact, it's the same question I asked before diving into how products like TradingView, Figma, Excalidraw, Google Maps and VS Code are built.
The answer is surprisingly nuanced.
Canvas is not a replacement for virtualization.
Instead, it's a different rendering engine that removes an entirely different set of browser bottlenecks.
Let's understand why.
The Problem
Imagine you're building a trading dashboard.
The requirements sound simple:
- 2 million rows
- 150 columns
- Real-time price updates
- Smooth scrolling
- Frozen columns
- Cell selection
- Keyboard navigation
- Sorting
- Filtering
Your first instinct is React.
<Table rows={rows} />
Everything works.
Until the dataset grows.
Now scrolling stutters.
Memory usage increases.
Updates become expensive.
Even with virtualization, the UI isn't as smooth as you expected.
Why?
Because rendering data is only one part of the problem.
Understanding the Browser Rendering Pipeline
Whenever React updates the UI, the browser performs much more work than many developers realize.
React State
│
▼
React Render
│
▼
Virtual DOM Diff
│
▼
DOM Updates
│
▼
Style Calculation
│
▼
Layout
│
▼
Paint
│
▼
Compositing
│
▼
Screen
Virtualization reduces the number of DOM nodes.
It does not eliminate the browser rendering pipeline.
Approach 1 — Render Everything
The most straightforward solution is:
<tbody>
{rows.map(renderRow)}
</tbody>
For 1 million rows this creates millions of DOM nodes.
The browser now has to maintain:
- DOM tree
- CSS tree
- Layout tree
- Paint tree
- Accessibility tree
Memory usage explodes.
Scrolling becomes unusable.
This approach doesn't scale.
Approach 2 — Pagination
Another common solution is pagination.
1 2 3 4 5 Next
Advantages:
- Small DOM
- Easy implementation
- Low memory usage
Disadvantages:
- Breaks natural scrolling
- Poor exploration of large datasets
- Doesn't feel like Excel
Pagination solves rendering, but sacrifices user experience.
Approach 3 — Virtualization
Virtualization changed everything.
Instead of rendering every row, we only render what's visible.
Dataset
────────────────────────
Row 0
Row 1
Row 2
...
Row 2,000,000
────────────────────────
Viewport
┌───────────────────┐
│ Row 1500 │
│ Row 1501 │
│ Row 1502 │
│ Row 1503 │
│ Row 1504 │
└───────────────────┘
Even if the dataset contains two million rows, the browser only creates around forty DOM rows.
This is an enormous improvement.
So why isn't that enough?
The Hidden Cost of the DOM
Suppose we render only 40 visible rows.
Each row contains:
- 10 cells
- Icon
- Badge
- Tooltip
- Status indicator
One row can easily become:
Row
├── Avatar
├── Name
├── Badge
├── Icon
├── Price
├── Status
├── Tooltip
└── Actions
Now multiply that by 40 rows.
Hundreds or even thousands of DOM nodes still exist.
Every update still triggers:
- Style recalculation
- Layout
- Paint
- Compositing
Virtualization reduces the amount of work.
It doesn't remove the browser rendering engine.
A Different Way to Think
Canvas changes one important thing.
Instead of asking the browser to manage UI objects, we simply tell it:
Draw pixels here.
The browser only knows about one element.
<canvas></canvas>
Everything inside the table becomes our responsibility.
There are no rows.
No cells.
No buttons.
No DOM tree.
Only pixels.
Immediate Mode vs Retained Mode
DOM uses Retained Mode Rendering.
The browser remembers every element.
DOM
Button
↓
Style
↓
Layout
↓
Paint
Canvas uses Immediate Mode Rendering.
Draw Rectangle
↓
Draw Text
↓
Draw Border
↓
Done
The browser doesn't remember anything.
It simply paints.
Building a Table with Canvas
A DOM table looks like this:
<td>$1250</td>
A Canvas table looks like this:
ctx.fillRect(x, y, width, height);
ctx.fillText("$1250", x + 10, y + 18);
Instead of creating objects, we're issuing drawing commands.
But Doesn't Canvas Redraw Everything?
Yes.
This is often viewed as a disadvantage.
Imagine 40 visible rows.
Canvas redraws all 40 rows.
At first glance this sounds expensive.
However...
Drawing 40 rectangles and 400 text labels is often significantly cheaper than asking the browser to:
- Update DOM
- Recalculate layout
- Paint
- Composite
Modern GPUs are exceptionally good at drawing pixels.
The Key Difference
Virtualization optimizes how much UI exists.
Canvas optimizes how the UI is rendered.
Those are completely different optimizations.
Comparing the Two Pipelines
React + Virtualization
User Scrolls
│
▼
React
│
▼
Virtual DOM
│
▼
DOM Updates
│
▼
Style
│
▼
Layout
│
▼
Paint
│
▼
Screen
Canvas Rendering
User Scrolls
│
▼
Viewport
│
▼
Visible Rows
│
▼
Draw Commands
│
▼
Canvas
│
▼
GPU
│
▼
Screen
Notice what's missing.
There is no CSS layout engine.
There is no DOM update.
There is no paint tree.
Why Canvas Doesn't Compute Layout
This is a common misconception.
People often assume Canvas computes layout on every frame.
It doesn't.
The browser computes layout.
Canvas simply draws.
Suppose every column has a fixed width.
Column Width = 120
Column = 5
x = 5 × 120 = 600
That's just multiplication.
No flexbox.
No CSS Grid.
No table layout algorithm.
No browser layout engine.
What About Data Updates?
Imagine a stock price changing.
1240
↓
1243
Both React and Canvas must compute the new value.
The difference happens afterwards.
React:
Data
↓
React
↓
DOM
↓
Layout
↓
Paint
Canvas:
Data
↓
fillText()
↓
Done
Canvas doesn't eliminate business logic.
It removes browser rendering overhead.
The Next Evolution: Workers
Rendering is only part of the problem.
Large applications also need to:
- Fetch data
- Parse JSON
- Sort
- Filter
- Search
- Aggregate
Doing all of that on the main thread creates jank.
Instead we can move data processing into a Worker.
Main Thread
│
User Input / Scrolling
│
▼
Canvas Renderer
▲
│
Message Channel
│
▼
Web Worker
┌─────────────────────┐
│ Fetch Data │
│ Parse JSON │
│ Filter │
│ Sort │
│ Search │
│ Cache │
└─────────────────────┘
Now the UI thread focuses almost entirely on interaction and rendering.
Could We Go Even Further?
Yes.
Modern browsers support OffscreenCanvas.
That means rendering itself can also move to a Worker in supported environments.
A future architecture could look like this:
Backend
│
▼
Data Source
│
▼
Web Worker
├── Fetch
├── Parse
├── Filter
├── Sort
├── Cache
├── Build Draw Commands
│
▼
OffscreenCanvas
│
▼
GPU
│
▼
Display
At this point the browser's main thread is almost entirely dedicated to user interaction.
When Should You Use Canvas?
Canvas is an excellent fit for:
- Trading terminals
- Financial dashboards
- Large analytical tables
- Whiteboards
- Diagram editors
- CAD tools
- Heatmaps
- GIS applications
- Real-time monitoring dashboards
It's the reason products like Figma, Excalidraw, Google Maps, and TradingView can remain responsive while rendering enormous amounts of visual information.
When Should You Not Use Canvas?
Canvas introduces complexity.
If your application contains:
- Forms
- Inputs
- Buttons
- Dropdowns
- Rich accessibility requirements
- Small datasets
React with virtualization is usually the better choice.
Canvas isn't a universal replacement.
It's a specialized rendering engine.
Final Thoughts
Virtualization solved one of the biggest challenges on the web: too many DOM nodes.
Canvas solves a different challenge: the cost of the browser rendering engine itself.
The two approaches are not competitors.
They're complementary.
Virtualization decides what should be rendered.
Canvas decides how it should be rendered.
Understanding that distinction changes how you think about frontend architecture.
And it opens the door to building interfaces that scale to millions of rows, thousands of updates per second, and experiences that feel closer to native desktop applications than traditional web pages.
In the next article, we'll start building a production-grade Canvas-based grid engine from scratch—one subsystem at a time.
Top comments (0)