DEV Community

Cover image for Rendering Massive Tables at Lightning Speed: Virtualization with Virtual Scrolling
Lalit
Lalit

Posted on

Rendering Massive Tables at Lightning Speed: Virtualization with Virtual Scrolling

When building data-intensive applications - such as analytics, dashboards, admin panels, or log viewers - one of the toughest challenges is efficiently rendering very large tabular data in web browsers. Imagine a table with 100,000 rows and 50 columns - even a powerful device can stutter or freeze when trying to paint all of that to the DOM at once.

Existing libraries like react-window or react-virtualized do offer solutions through list virtualization and they are good. However, they often come with framework overhead, limited flexibility, and a layer of abstraction that hides how the optimization actually works which resist you to do more optimization as per your needs and use case.

In this article, I’ll walk you through how I built a high-performance tabular data viewer using Virtualization with Virtual Scrolling, written entirely in Vanilla JavaScript with TypeScript — no frameworks, no magic. This approach provides better control, leaner performance, and a deeper understanding of how browsers handle large DOM structures.

You can try out the live demo here 👉 Live Demo (Select csv file to test) and checkout full code on Github.

⚠️ Disclaimer:
This article focuses solely on the rendering techniques, not on client side data management strategies. Data fetching, caching, and server synchronization are separate architectural challenges that I'll may be cover in future discussions.

🧩 The Browser Rendering Pipeline — From HTML to Pixels

Before diving into virtualization or other optimization techniques, it’s important to understand how browsers handle rendering. A web page goes through several distinct stages before pixels appear on the screen:

1. HTML parsing -> DOM Tree Construction

The browser reads you HTML and builds a tree-like structure called the DOM (Document Object Model). Each tag becomes a node.

2. CSS Parsing -> CSSOM Tree

CSS rules are parsed into another structure, the CSS Object Model (CSSOM).

3. Render Tree constructed

The DOM and CSSOM are merged into the Render Tree, which represents what actually needs to be painted - including styles, layout, and visibility.

4. Layout (Reflow)

The browser calculates the size and position of every element. This step is expensive.

5. Painting

The bowser paints individual elements onto layers.

6. Compositing

Layers are combined to display the final frame on the screen.

As, it can be seen that there are multiple steps involved from HTML to pixels. So, when we render thousands of elements, this pipeline becomes overwhelmed - every element must be laid out, painted and composited. Even if most are offscreen, the browser doesn't know that - it still works on them.

That's where Virtualization comes in.

⚡ The Concept of Virtualization (and Virtual Scrolling)

Virtualization is the technique of rendering only what the user can see. Instead of mounting all 100,000 rows to the DOM, you render just a small window — say, 50 rows — corresponding to the visible area. As the user scrolls, you keep adding or replacing new.

In essence:

“You’re faking a long list, while actually maintaining only a handful of visible elements.”

🧠 Virtual Scrolling — The “Fake Scroll”

Traditional virtualization often uses a real scrollable container (overflow: auto), but this can be inefficient when the DOM is updated frequently. My approach goes one step further — a “virtual” scroll simulation:

  • The table container has a fixed height and overflow: hidden.

  • Instead of relying on native scroll behavior, we listen to wheel events.

  • Based on the scroll delta, we shift the data window (start and end indices).

  • The visible cells are updated using textContent (will talk about why I have used textContent) instead of re-creating DOM nodes.

  • A custom scrollbar reflects scroll position and allows direct navigation.

  • This can be done for both horizontal as well as vertical

So the browser only repaints the same number of cells repeatedly, while the illusion of scrolling is created by updating their text and adjusting a scrollbar thumb position.

Figure: Simplified View of Virtual Scrolling

Simplified view of virtual scrolling

Even if your dataset has 1 million rows, you’re only maintaining a constant number of DOM nodes.

🔍 Why I am using textContent to update the cells content

In this approach, each cell’s content is updated through:

cell.textContent = dataRow[columnIndex];
Enter fullscreen mode Exit fullscreen mode

This choice may seem little, but it’s crucial for performance. That's why in the start of this article I have explained a bit of browser working. Let’s briefly compare the three main DOM update methods:

Method Description Performance
innerHTML Parses HTML string and replaces all child nodes. ❌ Slow — triggers reflow and DOM reconstruction.
innerText Sets or gets the rendered text (respects CSS). ⚠️ Slower — forces layout recalculation.
textContent Sets text directly on node without layout changes. ✅ Fast — minimal reflow, ideal for frequent updates.

Since virtual scrolling updates cells many times per second, textContent, I my opinion, is by far the most efficient choice in our case.

🧰 Why Vanilla JavaScript?

It’s tempting to reach for React, especially with their ecosystem of virtualized table components. However, I have found in my working that framework abstractions some time introduce overhead — reconciliation, synthetic events, and VDOM diffing and cause performance decay (I could be wrong happy to be corrected).

By going framework-free, we gain:

  • Direct control over event handling and rendering cycles.

  • No diffing — updates happen instantly via textContent.

  • Smaller bundle size, faster startup.

  • Perfect for embedding in micro frontends or low-resource devices.

🧭 Custom Scrollbar for Navigation

Instead of a native scrollbar, the demo implements a fake scrollbar — a styled div (thumb) within a track. Moving the thumb calculates which data slice should be displayed.

This design decouples scrolling behavior from the browser’s default implementation, allowing you to fine-tune scroll speed, inertia etc.

It’s also visually customizable — an advantage for embedded analytics dashboards or custom-styled admin tools.

🧮 Optimization Layers

Beyond the virtualization concept itself, there are several smaller optimizations that help maintain performance. Lets see few of them, which I have found crucial.

  • requestAnimationFrame() for updates:
    Defers updates to the next paint cycle to prevent layout thrashing.

  • debounce and throttle` utilities:
    Common performance utilities as they will smooth out rapid input or scroll events.

  • Minimal reflows:
    Important All rows and columns are fixed-size, avoid using min-width etc, use fixed size, so layout computation is predictable and cheap.

  • Data shape
    I have found that data shape that you are using to loop over and render must be efficient to access.

💬 Note:
In this demo, all data resides in memory. Large-scale datasets typically require lazy loading or server-side pagination or even if large amount of data needs to be on client's device then must to be efficiently stored and handled, which will be discussed in future articles about data management.

💡 Needed more performance then use Canvas based Tables

While this virtual scrolling approach is extremely efficient, the DOM still carries certain overhead. Each cell is still an HTML element. When datasets reach millions of cells, even 50 visible rows × 50 columns = 2,500 active nodes can stress the layout engine.

If you require absolute maximum performance, the next logical step would a Canvas-based Table:

  • Each cell is drawn as text on a 2D <canvas> surface.

  • No DOM nodes — just pixels.

  • Google Sheets also uses this approach.

Ideal for real-time dashboards or data visualization systems.

I’ll cover this “Canvas Table” approach as well in my next article as well.

🚀 Closing Thoughts

Virtualization isn’t a new idea, it's been in there and users are using it, but that also sometimes require changes and optimizations to make it more efficient.

Stay tuned for my next article — “Rendering Millions of Cells with Canvas: Beyond the DOM” — where I’ll push this concept even further using Canvas based Table.

If you found this concept interesting and want to explore more, I’d love to connect!

You can follow me on LinkedIn, GitHub, and Twitter — I often share experiments, open-source work, and performance insights there.

If you're interested in contributing or collaborating on this, share ideas and suggestions, please feel free to reach out — let’s build something exciting together!

Top comments (0)