= 1024) leftSidebarOpen = false; if(window.innerWidth >= 1280) rightSidebarOpen = false"<br>:class="{ 'dark': $store.theme.dark, 'left-sidebar-closed': !leftSidebarDesktop, 'right-sidebar-closed': !rightSidebarDesktop, 'overflow-hidden': leftSidebarOpen || rightSidebarOpen, 'sidebars-closed': !leftSidebarDesktop && !rightSidebarDesktop }">
The 10 Levels of Building a Data Grid
Skip to content
Navigation
if (window.innerWidth
Preferences
Layout
Save Sidebar view
Remember if sidebars are open or closed across pages.
Save Sidebar layout preference
Enable Bookmarks
Save your favourite articles locally in this browser.
Enable Local Bookmarks
let scroll = window.scrollY;<br>let docHeight = document.documentElement.scrollHeight - window.innerHeight;<br>this.progress = docHeight > 0 ? Math.max(0, Math.min(1, scroll / docHeight)) : 0;<br>this.ticking = false;<br>});<br>}"<br>@scroll.window.passive="update()"<br>@resize.window.passive="update()"<br>x-init="update()"<br>class="fixed top-0 left-0 w-full h-[3px] z-[100] pointer-events-none">
VisuaLeaf's Table View
Grids seem easy. It's just a table, right? A 2D array you display in HTML. Two nested loops, some CSS borders, done by lunch.
That's what I thought too. I've spent the past year building a database GUI from scratch (MongoDB first, PostgreSQL and other SQL databases later), and the table view alone ate around 9 months of on-and-off optimization work. The first real dataset is what humbled me: ten thousand documents, some with hundreds of fields, nested five levels deep, on a 4K monitor, and users expecting it to scroll like a native app. My two nested loops died instantly. Everything I tried after that kept teaching me the same lesson from a different angle: to make a grid fast you end up building most of a rendering engine, and the table part is almost incidental.
Every stage felt like discovering a level I didn't know existed until it was the only thing that mattered. A technique would solve the previous problem, then a flaw in that technique would become the next problem. So that's how I structured this post: ten levels, each ending roughly where the next begins. I've tried to explain the actual mechanisms, not just the morals, so that by the end you could build one of these yourself.
One thing before the levels: the spec, because it explains every design decision that follows. This was never going to be a display-only table. It had to understand every BSON type and JSONB (with icons color-coded by type, since "123" the string and 123 the int are different queries), expand nested documents into real child columns, and be searchable across those nested paths with matches highlighted inside the cells. Columns had to be reorderable, resizable, pinnable. Cells had to be editable in place. And I wanted to drag any value, row, or column straight out of the grid into my visual query builder. Every one of those features is state that has to live somewhere and survive scrolling, and none of it exists in a raw document. So yeah... this was gonna be a tough one.
Level 1. Brute force: render everything
Everyone starts here. Loop over the rows, loop over the fields, emit a cell. The framework re-renders when data changes. Ship it.
It works at 100 rows. At 10,000 rows and 30 columns you've asked the browser to build around 300,000 DOM nodes, and every framework change-detection pass walks all of them. Scrolling stutters, clicks lag, and eventually the tab just dies.
Two numbers explain why. A DOM node costs roughly a kilobyte once you count the browser's internal structures, so 300,000 nodes is hundreds of megabytes before your actual data. And a frame at 60fps is 16.7 milliseconds, shared with style, layout, and paint. Any single step that touches every node blows that budget by an order of magnitude.
So you learn the foundational thing: the DOM can't be where your data lives. It can only hold a small visible slice of it.
The obvious plan is to render just that slice. But a sliced renderer has a problem brute force never had: something now has to keep track of what to show and what not to show. Which rows are in view, which columns, in what order, which nested fields are expanded, what the search matched. Brute force never needed any of that, the DOM just held everything. The moment you render a slice, that knowledge needs a home, and the renderer needs to read it hundreds of times per second as content scrolls in and out. That home is level 2.
Level 2. The shadow table: The Foundation
The tempting answer is to keep all that "what to show" knowledge next to the raw documents and just read from them. But raw documents are terrible render inputs. MongoDB gives you nested, heterogeneous BSON (ObjectIds, Decimal128s, timestamps, binary), SQL brings JSONB and timestamps with zones, and every one of them needs a formatting decision per cell. If the render loop makes those decisions, you pay for them on every frame, forever. And the raw data doesn't know what the table looks like...