Rendering Performance: Reflow and Repaint in JavaScript
Rendering performance directly impacts user experience. This article covers understanding the rendering pipeline, identifying performance bottlenecks, and optimizing for smooth 60fps interactions.
Introduction
Modern browsers need to:
- Parse HTML and CSS
- Build the DOM and CSSOM
- Calculate layouts (reflow)
- Paint pixels (repaint)
- Composite layers
Understanding this process helps you:
- Achieve 60fps animations
- Reduce jank and stuttering
- Improve perceived performance
- Build responsive interfaces
The Rendering Pipeline
Critical Rendering Path
// The browser rendering pipeline:
// 1. Parse HTML โ DOM
// 2. Parse CSS โ CSSOM
// 3. Combine โ Render Tree
// 4. Layout (Reflow) โ Calculate positions
// 5. Paint (Repaint) โ Draw pixels
// 6. Composite โ Combine layers
// Example: Triggering the full pipeline
const element = document.getElementById('box');
// This triggers the full pipeline
element.style.width = '100px'; // Triggers reflow and repaint
element.style.backgroundColor = 'red'; // Triggers repaint
Frame Budget
// 60fps = 16.67ms per frame
// Budget breakdown:
// - JavaScript: ~10ms
// - Rendering: ~6ms
// - Browser overhead: ~0.67ms
// Example: Staying within frame budget
function animateWithinBudget() {
const startTime = performance.now();
// Do work within 10ms budget
while (performance.now() - startTime < 10) {
// Process data
}
// Request next frame
requestAnimationFrame(animateWithinBudget);
}
Reflow (Layout)
What Triggers Reflow
// Reading layout properties triggers reflow
const element = document.getElementById('box');
// These trigger reflow:
const width = element.offsetWidth; // Read
const height = element.offsetHeight; // Read
const top = element.offsetTop; // Read
const left = element.offsetLeft; // Read
const scrollTop = element.scrollTop; // Read
const scrollHeight = element.scrollHeight; // Read
const clientWidth = element.clientWidth; // Read
const clientHeight = element.clientHeight; // Read
// Writing layout properties triggers reflow
const element = document.getElementById('box');
// These trigger reflow:
element.style.width = '100px'; // Write
element.style.height = '100px'; // Write
element.style.position = 'absolute'; // Write
element.style.top = '10px'; // Write
element.style.left = '10px'; // Write
element.style.display = 'none'; // Write
element.style.visibility = 'hidden'; // Write
Layout Thrashing
// โ Bad: Layout thrashing (alternating reads and writes)
function badLayoutThrashing() {
const elements = document.querySelectorAll('.item');
elements.forEach(el => {
el.style.width = el.offsetWidth + 10 + 'px'; // Read then write
el.style.height = el.offsetHeight + 10 + 'px'; // Read then write
});
}
// Each element triggers reflow multiple times
// Total reflows: elements.length * 2
// โ
Good: Batch reads then writes
function goodLayoutOptimization() {
const elements = document.querySelectorAll('.item');
const measurements = [];
// Batch all reads
elements.forEach(el => {
measurements.push({
width: el.offsetWidth,
height: el.offsetHeight
});
});
// Batch all writes
elements.forEach((el, i) => {
el.style.width = measurements[i].width + 10 + 'px';
el.style.height = measurements[i].height + 10 + 'px';
});
}
// Total reflows: 1 (all reads batched, then all writes batched)
Practical Example: Avoiding Layout Thrashing
// โ Bad: Multiple reflows
function updatePositions() {
const boxes = document.querySelectorAll('.box');
boxes.forEach((box, index) => {
box.style.left = box.offsetLeft + 10 + 'px'; // Reflow
box.style.top = box.offsetTop + 10 + 'px'; // Reflow
});
}
// โ
Good: Single reflow
function updatePositionsOptimized() {
const boxes = document.querySelectorAll('.box');
const positions = [];
// Read all positions
boxes.forEach(box => {
positions.push({
left: box.offsetLeft,
top: box.offsetTop
});
});
// Update all positions
boxes.forEach((box, index) => {
box.style.left = positions[index].left + 10 + 'px';
box.style.top = positions[index].top + 10 + 'px';
});
}
Repaint
What Triggers Repaint
// These trigger repaint (but not reflow):
const element = document.getElementById('box');
element.style.color = 'red'; // Repaint
element.style.backgroundColor = 'blue'; // Repaint
element.style.borderColor = 'green'; // Repaint
element.style.opacity = 0.5; // Repaint
element.style.boxShadow = '0 0 10px black'; // Repaint
element.style.textShadow = '0 0 5px gray'; // Repaint
Optimizing Repaints
// โ Bad: Multiple repaints
function badRepaint() {
const element = document.getElementById('box');
element.style.color = 'red';
element.style.backgroundColor = 'blue';
element.style.borderColor = 'green';
element.style.opacity = 0.5;
}
// โ
Good: Batch with class
function goodRepaint() {
const element = document.getElementById('box');
element.classList.add('highlighted');
}
// CSS:
// .highlighted {
// color: red;
// background-color: blue;
// border-color: green;
// opacity: 0.5;
// }
Composite Layers
GPU Acceleration
// โ
Good: Use transform for GPU acceleration
function animateWithTransform() {
const element = document.getElementById('box');
let x = 0;
function animate() {
x += 5;
// Transform uses GPU, no reflow/repaint
element.style.transform = `translateX(${x}px)`;
if (x < 500) {
requestAnimationFrame(animate);
}
}
animate();
}
// โ Bad: Animating left property
function animateWithLeft() {
const element = document.getElementById('box');
let x = 0;
function animate() {
x += 5;
// Left property triggers reflow and repaint
element.style.left = x + 'px';
if (x < 500) {
requestAnimationFrame(animate);
}
}
animate();
}
Creating Composite Layers
// โ
Good: Create composite layer with will-change
const element = document.getElementById('animated-box');
element.style.willChange = 'transform';
// Later, animate with transform
element.style.transform = 'translateX(100px)';
// Clean up
element.style.willChange = 'auto';
// โ
Good: Use transform3d to force GPU acceleration
function forceGPUAcceleration() {
const element = document.getElementById('box');
// This creates a composite layer
element.style.transform = 'translate3d(0, 0, 0)';
// Now animate with transform
element.style.transform = 'translate3d(100px, 100px, 0)';
}
RequestAnimationFrame
Proper Animation Loop
// โ
Good: Use requestAnimationFrame
function properAnimation() {
let position = 0;
function animate() {
position += 5;
// Update DOM
document.getElementById('box').style.transform = `translateX(${position}px)`;
if (position < 500) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
}
// โ Bad: Using setInterval
function badAnimation() {
let position = 0;
setInterval(() => {
position += 5;
document.getElementById('box').style.left = position + 'px';
}, 16); // Doesn't sync with browser refresh rate
}
Coordinating Multiple Animations
// โ
Good: Single animation loop for multiple elements
function coordinatedAnimations() {
const elements = document.querySelectorAll('.animated');
let progress = 0;
function animate() {
progress += 0.01;
elements.forEach((el, index) => {
const offset = index * 50;
el.style.transform = `translateX(${Math.sin(progress + offset) * 100}px)`;
});
if (progress < Math.PI * 2) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
}
Practical Optimization Techniques
1. Debounce Resize Events
// โ Bad: Resize handler fires on every pixel change
window.addEventListener('resize', () => {
// Expensive layout calculations
recalculateLayout();
});
// โ
Good: Debounce resize events
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn(...args), delay);
};
}
window.addEventListener('resize', debounce(() => {
recalculateLayout();
}, 250));
2. Throttle Scroll Events
// โ
Good: Throttle scroll events
function throttle(fn, delay) {
let lastCall = 0;
return function(...args) {
const now = Date.now();
if (now - lastCall >= delay) {
fn(...args);
lastCall = now;
}
};
}
window.addEventListener('scroll', throttle(() => {
updateScrollPosition();
}, 16)); // ~60fps
3. Use Intersection Observer
// โ
Good: Intersection Observer for lazy loading
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadImage(entry.target);
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
function loadImage(img) {
img.src = img.dataset.src;
}
4. Virtual Scrolling
// โ
Good: Virtual scrolling for large lists
class VirtualScroller {
constructor(container, items, itemHeight) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.visibleItems = [];
this.container.addEventListener('scroll', () => this.updateVisibleItems());
this.updateVisibleItems();
}
updateVisibleItems() {
const scrollTop = this.container.scrollTop;
const containerHeight = this.container.clientHeight;
const startIndex = Math.floor(scrollTop / this.itemHeight);
const endIndex = Math.ceil((scrollTop + containerHeight) / this.itemHeight);
this.renderItems(startIndex, endIndex);
}
renderItems(startIndex, endIndex) {
this.container.innerHTML = '';
for (let i = startIndex; i < endIndex && i < this.items.length; i++) {
const item = document.createElement('div');
item.style.height = this.itemHeight + 'px';
item.textContent = this.items[i];
this.container.appendChild(item);
}
}
}
5. Batch DOM Updates
// โ Bad: Multiple DOM updates
function badBatchUpdate() {
const container = document.getElementById('container');
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
container.appendChild(div); // Triggers reflow each time
}
}
// โ
Good: Use DocumentFragment
function goodBatchUpdate() {
const container = document.getElementById('container');
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
fragment.appendChild(div);
}
container.appendChild(fragment); // Single reflow
}
Profiling Rendering Performance
Using DevTools
// Steps to profile rendering:
// 1. Open DevTools โ Performance tab
// 2. Click Record
// 3. Perform actions
// 4. Click Stop
// 5. Look for:
// - Long tasks (yellow)
// - Rendering (purple)
// - Painting (green)
// Example: Code to profile
function profileMe() {
const elements = document.querySelectorAll('.item');
elements.forEach(el => {
el.style.width = el.offsetWidth + 10 + 'px';
});
}
Measuring Frame Rate
// Measure actual frame rate
class FrameRateMonitor {
constructor() {
this.frames = 0;
this.lastTime = performance.now();
this.fps = 0;
}
update() {
this.frames++;
const now = performance.now();
const elapsed = now - this.lastTime;
if (elapsed >= 1000) {
this.fps = this.frames;
this.frames = 0;
this.lastTime = now;
console.log(`FPS: ${this.fps}`);
}
requestAnimationFrame(() => this.update());
}
start() {
this.update();
}
}
// Usage
const monitor = new FrameRateMonitor();
monitor.start();
Best Practices
-
Batch DOM reads and writes:
// โ Good const width = element.offsetWidth; element.style.width = width + 10 + 'px'; -
Use transform for animations:
// โ Good element.style.transform = 'translateX(100px)'; -
Use requestAnimationFrame:
// โ Good requestAnimationFrame(() => { element.style.transform = 'translateX(100px)'; }); -
Debounce/throttle events:
// โ Good window.addEventListener('scroll', throttle(handler, 16));
Common Mistakes
-
Layout thrashing:
// โ Bad elements.forEach(el => { el.style.width = el.offsetWidth + 10 + 'px'; }); -
Animating layout properties:
// โ Bad element.style.left = x + 'px'; // โ Good element.style.transform = `translateX(${x}px)`; -
Not using requestAnimationFrame:
// โ Bad setInterval(() => animate(), 16); // โ Good requestAnimationFrame(animate);
Summary
Rendering performance is critical for user experience. Key takeaways:
- Understand the rendering pipeline (reflow, repaint, composite)
- Batch DOM reads and writes
- Use transform for animations (GPU acceleration)
- Use requestAnimationFrame for smooth animations
- Debounce/throttle event handlers
- Profile with DevTools
- Aim for 60fps (16.67ms per frame)
Related Resources
- Rendering Performance - Google
- Reflow and Repaint - MDN
- RequestAnimationFrame - MDN
- Intersection Observer - MDN
- CSS Transforms - MDN
Next Steps
- Learn about Code Splitting and Lazy Loading
- Explore Performance Profiling: DevTools and Metrics
- Study Memory Optimization and Leak Detection
- Practice optimizing rendering in your applications
- Profile and measure frame rates
Comments