Skip to main content
โšก Calmops

Rendering Performance: Reflow and Repaint in JavaScript

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

  1. Batch DOM reads and writes:

    // โœ… Good
    const width = element.offsetWidth;
    element.style.width = width + 10 + 'px';
    
  2. Use transform for animations:

    // โœ… Good
    element.style.transform = 'translateX(100px)';
    
  3. Use requestAnimationFrame:

    // โœ… Good
    requestAnimationFrame(() => {
      element.style.transform = 'translateX(100px)';
    });
    
  4. Debounce/throttle events:

    // โœ… Good
    window.addEventListener('scroll', throttle(handler, 16));
    

Common Mistakes

  1. Layout thrashing:

    // โŒ Bad
    elements.forEach(el => {
      el.style.width = el.offsetWidth + 10 + 'px';
    });
    
  2. Animating layout properties:

    // โŒ Bad
    element.style.left = x + 'px';
    
    // โœ… Good
    element.style.transform = `translateX(${x}px)`;
    
  3. 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)

Next Steps

Comments