Table of Contents#
- Understanding HTML Tables and
innerHTML - How Adding Elements with
innerHTMLWorks (and Why It’s Fast) - Why Removing Elements with
innerHTMLFeels Slower: The Hidden Workflow - The DOM, Reflow, and Repaint: The Real Culprits Behind Sluggishness
- Case Study: Adding vs. Removing Rows—A Performance Showdown
- Better Alternatives: Beyond
innerHTMLfor Dynamic Tables - Best Practices to Optimize Table Performance
- Conclusion
- References
1. Understanding HTML Tables and innerHTML#
Before we compare adding and removing elements, let’s ground ourselves in the basics: HTML tables and the innerHTML property.
What Are HTML Tables?#
HTML tables are structured elements designed to display tabular data, with a hierarchy of <table>, <thead>, <tbody>, <tr> (rows), and <td> (cells). They’re widely used for grids, spreadsheets, and data-heavy interfaces due to their built-in semantics and layout stability.
Example table structure:
<table id="dataTable">
<thead>
<tr><th>ID</th><th>Name</th></tr>
</thead>
<tbody id="tableBody">
<tr><td>1</td><td>Alice</td></tr>
<tr><td>2</td><td>Bob</td></tr>
</tbody>
</table> What Is innerHTML?#
The innerHTML property allows you to read or set the HTML content of an element as a string. For example, to add a row to tableBody, you might do:
const tableBody = document.getElementById('tableBody');
tableBody.innerHTML += '<tr><td>3</td><td>Charlie</td></tr>'; // Append a new row Developers love innerHTML for its simplicity: it lets you manipulate complex DOM structures with a single string, avoiding verbose createElement/appendChild calls. But this convenience comes with tradeoffs—especially when removing elements.
2. How Adding Elements with innerHTML Works (and Why It’s Fast)#
Adding elements with innerHTML often feels fast. Let’s break down why.
The Workflow of Adding with innerHTML#
When you append content via innerHTML += ..., the browser follows these steps:
- Read the current HTML of the target element (e.g.,
tableBody). - Concatenate the new HTML string (the new row) to the existing content.
- Parse the combined string into a new DOM subtree.
- Replace the old subtree with the new one (appending the new row).
While this sounds like a lot, modern browsers optimize this process:
- Batched parsing: Browsers parse HTML efficiently, especially for small additions (e.g., a single row).
- Minimal reflow: Appending a row to the end of a table often triggers only a single reflow (layout recalculation) since the table’s width/height may grow predictably, and existing content isn’t disturbed.
Why It Feels Fast#
Adding is fast because:
- You’re appending to existing content, so the browser doesn’t need to reprocess the entire table—just the new row.
- The DOM subtree change is incremental, and reflow/repaint costs are minimized.
3. Why Removing Elements with innerHTML Feels Slower: The Hidden Workflow#
Removing elements with innerHTML is a different beast. To understand why it’s slower, let’s walk through a typical removal scenario.
The Workflow of Removing with innerHTML#
Suppose you want to remove the second row (<tr>) from tableBody. With innerHTML, you might:
- Read the entire current HTML of
tableBody(e.g., a string like<tr>...</tr><tr>...</tr><tr>...</tr>). - Manipulate the string to remove the target row (e.g., split the string by
</tr>, remove the unwanted segment, then rejoin). - Parse the modified string into a brand-new DOM subtree (rebuilding all remaining rows from scratch).
- Replace the entire old subtree with the new one.
The Costly Difference: Rebuilding vs. Appending#
Here’s why this is slower than adding:
- Full subtree reconstruction: Unlike adding (which appends), removing requires rebuilding the entire DOM subtree of
tableBody. Even if you’re removing one row, the browser discards all existing rows and recreates the remaining ones from the modified HTML string. - String manipulation overhead: Parsing and modifying large HTML strings (e.g., a table with 1,000 rows) is slow in JavaScript, especially if done naively (e.g., using
split/join). - Garbage collection: The old DOM nodes (rows) are discarded, triggering garbage collection (GC) pauses, which feel like lag.
4. The DOM, Reflow, and Repaint: The Real Culprits Behind Sluggishness#
To truly grasp the slowness, we need to understand the browser’s rendering pipeline: reflow and repaint.
Reflow (Layout Calculation)#
Reflow is the process where the browser recalculates the geometry of elements (size, position, etc.). It’s triggered by DOM changes that affect layout (e.g., adding/removing rows, changing dimensions).
- Adding a row: Typically triggers a single reflow, as the table grows downward.
- Removing a row: If the row is in the middle or top, the browser must reflow all subsequent rows (e.g., shifting them up). For large tables, this is expensive.
Repaint (Pixel Rendering)#
Repaint is when the browser updates the screen with new pixels. Even if layout doesn’t change (no reflow), repaint is needed if visual styles change (e.g., color).
- Adding: Repaints only the new row.
- Removing: May repaint a larger area (e.g., the entire table if rows shift).
innerHTML and Reflow: A Perfect Storm#
When you use innerHTML to remove a row, you’re not just deleting a node—you’re rebuilding the entire table body. This triggers:
- Full reflow of the table (since all remaining rows are recreated).
- Full repaint of the table (since the visual layout has changed).
For large tables, this is a massive performance hit compared to adding, which may only trigger a partial reflow/repaint.
5. Case Study: Adding vs. Removing Rows—A Performance Showdown#
Let’s test this with code. We’ll create a table with 1,000 rows and measure the time to add 100 rows vs. remove 100 rows using innerHTML.
Setup#
<table id="testTable">
<tbody id="testBody"></tbody>
</table> // Helper: Generate a table with N rows
function populateTable(rows) {
let html = '';
for (let i = 0; i < rows; i++) {
html += `<tr><td>Row ${i}</td></tr>`;
}
testBody.innerHTML = html;
}
// Initial population: 1,000 rows
populateTable(1000); Test 1: Adding 100 Rows with innerHTML#
const startAdd = performance.now();
// Append 100 rows
let newRows = '';
for (let i = 1000; i < 1100; i++) {
newRows += `<tr><td>Row ${i}</td></tr>`;
}
testBody.innerHTML += newRows;
const endAdd = performance.now();
console.log(`Add time: ${endAdd - startAdd}ms`); // ~12ms (Chrome) Test 2: Removing 100 Rows with innerHTML#
const startRemove = performance.now();
// Get current rows as HTML string
let currentHtml = testBody.innerHTML;
// Split into rows, remove first 100, rejoin
const rows = currentHtml.split('</tr>').filter(row => row.trim() !== '');
const newRows = rows.slice(100).join('</tr>') + '</tr>'; // Re-add closing tag
testBody.innerHTML = newRows;
const endRemove = performance.now();
console.log(`Remove time: ${endRemove - startRemove}ms`); // ~45ms (Chrome) Results#
Adding 100 rows took ~12ms; removing 100 rows took ~45ms—3.75x slower!
Why? Because removing required:
- Parsing 1,000 rows of HTML.
- Manipulating a large string.
- Rebuilding 900 rows from scratch.
- Triggering a full reflow/repaint of the table.
6. Better Alternatives: Beyond innerHTML for Dynamic Tables#
innerHTML is convenient, but for removal, it’s inefficient. Here are better approaches:
1. Direct DOM Manipulation (removeChild)#
Instead of rebuilding the entire subtree, target the specific row and remove it:
// Remove the 5th row (index 4)
const rowToRemove = tableBody.children[4];
tableBody.removeChild(rowToRemove); Why it’s better:
- Only the target row is removed; no full subtree rebuild.
- Minimal reflow/repaint (only the affected area).
2. Document Fragments for Batch Operations#
For removing multiple rows, use DocumentFragment to batch DOM changes:
const fragment = document.createDocumentFragment();
// Add remaining rows to fragment (skip rows to remove)
Array.from(tableBody.children).forEach((row, index) => {
if (index % 2 !== 0) fragment.appendChild(row); // Keep even rows
});
// Replace tableBody with fragment (single reflow)
tableBody.innerHTML = '';
tableBody.appendChild(fragment); Why it’s better:
- Batches changes into a single reflow/repaint.
3. Virtual DOM Libraries (React, Vue, Svelte)#
Libraries like React use a virtual DOM to minimize real DOM updates. They diff changes and only update what’s necessary, making removal as fast as addition.
7. Best Practices to Optimize Table Performance#
To keep your tables fast:
- Avoid
innerHTMLfor removal: UseremoveChildorDocumentFragmentinstead. - Batch DOM changes: Minimize reflows by making all changes at once (e.g., hide the table, modify, then show it).
- Use
table-layout: fixed: This fixes column widths, reducing reflow costs. - Debounce rapid removals: If users can delete rows quickly (e.g., via a button), debounce to avoid excessive reflows.
8. Conclusion#
Removing elements with innerHTML feels slower than adding because:
- Removal requires rebuilding the entire DOM subtree of the table body, triggering full reflow/repaint.
- String manipulation and garbage collection add overhead.
By switching to direct DOM methods (e.g., removeChild) or virtual DOM libraries, you can make removal as fast as addition. The key is to minimize full subtree reconstruction and reduce reflow/repaint costs.