š³
Intermediate
11 min readDOM Manipulation Performance
Efficient DOM operations, reflows, repaints, and layout optimization
Understanding Browser Rendering
The browser rendering process involves several steps: DOM construction, style calculation, layout (reflow), paint, and compositing. Understanding these steps is crucial for performance optimization.
Reflows vs Repaints
- Reflow: Recalculates element positions and dimensions (expensive)
- Repaint: Redraws pixels (less expensive than reflow)
- Composite: Combines layers (cheapest)
Layout Thrashing
Layout thrashing occurs when you repeatedly read and write to the DOM, forcing multiple reflows:
// ā Bad: Layout thrashing - reading and writing in a loop
function resizeElements() {
elements.forEach(element => {
const width = element.offsetWidth; // Read (forces reflow)
element.style.width = width + 10 + 'px'; // Write (invalidates layout)
});
}
// ā
Good: Batch reads, then batch writes
function resizeElementsOptimized() {
// First, read all values
const widths = elements.map(el => el.offsetWidth);
// Then, write all values
elements.forEach((element, i) => {
element.style.width = widths[i] + 10 + 'px';
});
}
// ā
Best: Use CSS transforms instead of layout properties
function moveElement(element, x, y) {
// ā Bad: Triggers reflow
element.style.left = x + 'px';
element.style.top = y + 'px';
// ā
Good: Uses compositing only
element.style.transform = `translate(${x}px, ${y}px)`;
}
Efficient DOM Manipulation
// ā Bad: Multiple DOM insertions
function addItems(items) {
const container = document.getElementById('container');
items.forEach(item => {
const div = document.createElement('div');
div.textContent = item;
container.appendChild(div); // Multiple reflows
});
}
// ā
Good: Use DocumentFragment
function addItemsOptimized(items) {
const fragment = document.createDocumentFragment();
items.forEach(item => {
const div = document.createElement('div');
div.textContent = item;
fragment.appendChild(div);
});
container.appendChild(fragment); // Single reflow
}
// ā
Better: Use innerHTML for large updates
function addItemsWithHTML(items) {
const html = items.map(item => `${item}`).join('');
container.innerHTML = html; // Single reflow
}
// ā
Best: Use insertAdjacentHTML
function addItemsEfficient(items) {
const html = items.map(item => `${item}`).join('');
container.insertAdjacentHTML('beforeend', html);
}
CSS Properties that Trigger Reflows
// Properties that trigger reflow (expensive):
// width, height, margin, padding, border, position, top, left, bottom, right
// display, float, clear, overflow, font-size, line-height
// Properties that only trigger repaint (cheaper):
// color, background-color, visibility, box-shadow, outline
// Properties that only trigger composite (cheapest):
// transform, opacity
// ā
Example: Animating with transform
@keyframes slide {
from { transform: translateX(0); }
to { transform: translateX(100px); }
}
// ā Avoid animating layout properties
@keyframes slideWrong {
from { left: 0; }
to { left: 100px; }
}
Measuring Performance
// Detect layout thrashing
function measureReflows() {
let reflowCount = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'Layout') {
reflowCount++;
}
}
});
observer.observe({ entryTypes: ['measure'] });
// Your DOM operations here
console.log('Reflows:', reflowCount);
}
// Use requestAnimationFrame for smooth updates
function updateUI() {
requestAnimationFrame(() => {
// All DOM reads
const height = element.offsetHeight;
const width = element.offsetWidth;
// All DOM writes
element.style.height = height * 2 + 'px';
element.style.width = width * 2 + 'px';
});
}
Virtual DOM Patterns
// Implement simple virtual DOM diffing
class VirtualDOM {
constructor() {
this.current = null;
}
render(vnode, container) {
if (!this.current) {
// Initial render
container.appendChild(this.createElement(vnode));
this.current = vnode;
} else {
// Update: diff and patch
this.updateElement(container, vnode, this.current);
this.current = vnode;
}
}
createElement(vnode) {
if (typeof vnode === 'string') {
return document.createTextNode(vnode);
}
const el = document.createElement(vnode.tag);
// Set attributes
Object.keys(vnode.attrs || {}).forEach(key => {
el.setAttribute(key, vnode.attrs[key]);
});
// Append children
(vnode.children || []).forEach(child => {
el.appendChild(this.createElement(child));
});
return el;
}
updateElement(parent, newNode, oldNode, index = 0) {
// Implement minimal DOM updates
if (!oldNode) {
parent.appendChild(this.createElement(newNode));
} else if (!newNode) {
parent.removeChild(parent.childNodes[index]);
} else if (this.changed(newNode, oldNode)) {
parent.replaceChild(
this.createElement(newNode),
parent.childNodes[index]
);
}
}
changed(node1, node2) {
return typeof node1 !== typeof node2 ||
(typeof node1 === 'string' && node1 !== node2) ||
node1.tag !== node2.tag;
}
}
Best Practices
- Batch DOM reads and writes separately
- Use
transformandopacityfor animations - Minimize forced synchronous layouts (reading offsetWidth, getComputedStyle)
- Use
DocumentFragmentfor multiple insertions - Avoid inline styles - use CSS classes instead
- Use
will-changeCSS property sparingly for animated elements - Debounce resize and scroll event handlers
- Use
content-visibility: autofor off-screen content - Implement virtual scrolling for long lists
- Cache DOM queries - don't repeatedly query for the same element