How do you optimize DOM manipulation?

Performance

The short answer

DOM manipulation is one of the most expensive operations in the browser. To optimize it, batch your changes, minimize direct DOM access, use document fragments for bulk inserts, avoid layout thrashing, and prefer CSS classes over inline style changes.

Why DOM manipulation is slow

Every time you modify the DOM, the browser may need to:

  1. Recalculate styles (which elements are affected)
  2. Reflow (recalculate positions and sizes)
  3. Repaint (redraw the affected pixels)

A single change is fast. But many changes in a row can cause the browser to redo these steps repeatedly, making the page slow and janky.

Optimization techniques

1. Batch DOM reads and writes separately

// Bad — alternating reads and writes (layout thrashing)
elements.forEach((el) => {
const height = el.offsetHeight; // read (forces layout)
el.style.height = height * 2 + 'px'; // write (invalidates layout)
});
// Good — all reads first, then all writes
const heights = elements.map((el) => el.offsetHeight);
elements.forEach((el, i) => {
el.style.height = heights[i] * 2 + 'px';
});

2. Use document fragments for bulk inserts

// Bad — 1000 individual DOM insertions
for (let i = 0; i < 1000; i++) {
const item = document.createElement('div');
item.textContent = `Item ${i}`;
container.appendChild(item); // reflow on each insert
}
// Good — one insertion
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const item = document.createElement('div');
item.textContent = `Item ${i}`;
fragment.appendChild(item);
}
container.appendChild(fragment); // one reflow

3. Use CSS classes instead of inline styles

// Bad — multiple style changes, multiple potential reflows
element.style.width = '100px';
element.style.height = '100px';
element.style.backgroundColor = 'red';
// Good — one class, one reflow
element.classList.add('box-active');

4. Minimize DOM queries

// Bad — queries the DOM on every iteration
for (let i = 0; i < 100; i++) {
document.querySelector('#counter').textContent = i;
}
// Good — cache the reference
const counter = document.querySelector('#counter');
for (let i = 0; i < 100; i++) {
counter.textContent = i;
}

5. Use event delegation

Instead of adding event listeners to every child element, add one to the parent:

// Bad — 100 event listeners
items.forEach((item) => {
item.addEventListener('click', handleClick);
});
// Good — 1 event listener
list.addEventListener('click', (e) => {
if (e.target.matches('.item')) {
handleClick(e);
}
});

6. Use requestAnimationFrame for visual updates

// Bad — update might happen mid-frame, causing jank
window.addEventListener('scroll', () => {
element.style.transform = `translateY(${window.scrollY}px)`;
});
// Good — update aligned with the browser's refresh cycle
window.addEventListener('scroll', () => {
requestAnimationFrame(() => {
element.style.transform = `translateY(${window.scrollY}px)`;
});
});

Interview Tip

Focus on the most impactful techniques: batching reads and writes (avoid layout thrashing), document fragments for bulk inserts, and event delegation. These are the ones that make the biggest difference in real applications. If you can explain why each technique helps (fewer reflows, fewer event listeners, fewer DOM queries), it shows you understand the underlying browser mechanics.

Why interviewers ask this

DOM performance is critical for smooth user experiences. Interviewers ask this to see if you know the common bottlenecks and how to avoid them. Even in React applications where you do not manipulate the DOM directly, understanding these principles helps you write better components and debug performance issues.