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'; ```javascript - Use transform for animations:
// ✅ Good element.style.transform = 'translateX(100px)'; ```javascript - Use requestAnimationFrame:
// ✅ Good requestAnimationFrame(() => { element.style.transform = 'translateX(100px)'; }); ```javascript - Debounce/throttle events:
// ✅ Good window.addEventListener('scroll', throttle(handler, 16)); ```javascript
Common Mistakes
- Layout thrashing:
// ❌ Bad elements.forEach(el => { el.style.width = el.offsetWidth + 10 + 'px'; }); ```javascript - Animating layout properties:
// ❌ Bad element.style.left = x + 'px'; // ✅ Good element.style.transform = `translateX(${x}px)`; ```javascript - 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