Chapter 01 of 34

Why big tables freeze the browser

One <tr> per row works for 100 rows. For 100,000 rows it blocks the browser for minutes.

After this chapter you can

  • measure what a plain HTML table costs
  • understand why time grows faster than the row count
  • know what a data grid has to do besides drawing

The problem

You have to show orders, measurements, log lines, positions – and there are many of them. The obvious solution is a plain HTML table: one <tr> per record, one <td> per value. It is simple, it is accessible, and for a few hundred rows it is perfect.

Here it is, written in the most straightforward way – the code from the first video:

naive-table.ts
function render(rows: Order[]): void {
table.innerHTML = "";
const header = table.createTHead().insertRow();
for (const col of columns) {
const th = header.appendChild(document.createElement("th"));
th.textContent = col;
th.onclick = () => sortBy(col);
}
const tbody = table.createTBody();
for (const order of rows) { // one <tr> per order …
const tr = tbody.insertRow();
for (const value of Object.values(order)) { // … one <td> per value
tr.insertCell().textContent = String(value);
}
}
}

Try it

Start the demo with 1,000 rows, then try 5,000 and 20,000. Click a column header to sort. Watch the numbers in the toolbar – and how long the page does not react at all.

Live The naive table
Screenshot: The naive table Screenshot: The naive table
20,000 rows block this page for several seconds. The step-by-step demo goes up to 100,000 rows – that takes minutes.

The numbers

Measured in Chromium (headless, Linux VM, October 2026) until the browser has really painted:

RowsDOM elementsFirst renderSort (rebuild everything)
1,00012,0140.2 s0.2 s
10,000120,0142.7 s2.9 s
25,000300,01411.7 s11.9 s
50,000600,01441.5 s44.4 s
100,0001,200,014> 2 min (browser blocked)–

Twice the rows does not mean twice the time: from 25,000 to 50,000 rows the render time grows by a factor of 3.5. Layout, style calculation and memory grow faster than the number of rows. And sorting is just as expensive as the first render, because every cell has to be built again:

function sortBy(col: keyof Order): void {
asc = col === sortCol ? !asc : true;
sortCol = col;
measure(`sort by ${col}`, () => {
// sorting itself takes milliseconds – building every cell again is what hurts
orders.sort((a, b) => (a[col] < b[col] ? -1 : a[col] > b[col] ? 1 : 0) * (asc ? 1 : -1));
render(orders);
});
}

And that is only displaying

A real data grid needs much more than drawing rows. All of this is still missing:

Next

The way out is a simple idea: draw only what is visible. In chapter 2 (Virtual scrolling) you build it yourself in about 20 lines – and see why you still want a library for it.