Skip to main content

Rendering Performance: Reflow and Repaint in JavaScript

Created: May 8, 2026 Larry Qu 8 min read

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';
    ```javascript
    
  2. Use transform for animations:
    // ✅ Good
    element.style.transform = 'translateX(100px)';
    ```javascript
    
  3. Use requestAnimationFrame:
    // ✅ Good
    requestAnimationFrame(() => {
      element.style.transform = 'translateX(100px)';
    });
    ```javascript
    
  4. Debounce/throttle events:
    // ✅ Good
    window.addEventListener('scroll', throttle(handler, 16));
    ```javascript
    

Common Mistakes

  1. Layout thrashing:
    // ❌ Bad
    elements.forEach(el => {
      el.style.width = el.offsetWidth + 10 + 'px';
    });
    ```javascript
    
  2. Animating layout properties:
    // ❌ Bad
    element.style.left = x + 'px';
    
    // ✅ Good
    element.style.transform = `translateX(${x}px)`;
    ```javascript
    
  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

Resources

Comments

Share this article

Scan to read on mobile