Front-end Engineering Lab

Paint Optimization

Reduce paint time and minimize repaint operations

Paint Optimization

Paint is when the browser draws pixels to the screen. Expensive paint operations cause jank and slow rendering. This guide covers how to minimize paint time.

Understanding Paint

Rendering Pipeline

JavaScript → Style → Layout → Paint → Composite

                           Expensive!

What Triggers Paint?

Properties that trigger paint:
- color
- background-color
- border-color
- box-shadow
- text-shadow
- visibility
- outline
- background (image, position, size)

What Doesn't Trigger Paint?

Compositor-only (cheap):
- transform
- opacity

Measuring Paint Performance

Chrome DevTools

1. Open DevTools
2. Performance tab
3. Check "Enable advanced paint instrumentation"
4. Record
5. Look for green "Paint" bars
6. Click to see paint regions

Paint Flashing

DevTools → Rendering → Paint flashing
Green overlay shows repainted regions

Layers Panel

DevTools → Layers (tab)
Shows:
- Composite layers
- Memory usage
- Paint count
- Which elements are on GPU

Reducing Paint Operations

1. Use Transform Instead of Layout Properties

/* ❌ BAD: Triggers paint */
.box {
  transition: width 0.3s, background-color 0.3s;
}
.box:hover {
  width: 200px;
  background-color: blue;
}

/* ✅ GOOD: Compositor-only */
.box {
  transition: transform 0.3s, opacity 0.3s;
}
.box:hover {
  transform: scaleX(1.5);
}
.box::before {
  content: '';
  position: absolute;
  inset: 0;
  background: blue;
  opacity: 0;
  transition: opacity 0.3s;
}
.box:hover::before {
  opacity: 1;
}

2. Promote Elements to Layers

Force GPU rendering for frequently changing elements.

.animated-element {
  will-change: transform;
  /* or */
  transform: translateZ(0);
}

Warning: Don't overuse! Each layer uses memory.

3. Reduce Paint Area

/* Limit paint to specific region */
.container {
  contain: paint;
  /* Painting won't escape container bounds */
}

4. Optimize Shadows

/* ❌ BAD: Large blur radius (expensive) */
.box {
  box-shadow: 0 10px 100px rgba(0, 0, 0, 0.5);
}

/* ✅ GOOD: Smaller blur (faster) */
.box {
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

/* ✅ BETTER: Use pseudo-element on separate layer */
.box {
  position: relative;
}
.box::after {
  content: '';
  position: absolute;
  inset: 0;
  box-shadow: 0 10px 100px rgba(0, 0, 0, 0.5);
  will-change: transform;
  z-index: -1;
}

5. Use CSS Containment

.card {
  contain: layout style paint;
}

/* Strictest */
.component {
  contain: strict;
}

Avoiding Unnecessary Repaints

1. Avoid :hover on Large Areas

/* ❌ BAD: Repaints entire table on mouse move */
table tr:hover {
  background: #f0f0f0;
}

/* ✅ GOOD: Limit to specific cells */
table td:hover {
  background: #f0f0f0;
}

2. Batch Style Changes

// ❌ BAD: Multiple repaints
element.style.color = 'red';
element.style.background = 'blue';
element.style.border = '1px solid black';

// ✅ GOOD: Single repaint
element.style.cssText = `
  color: red;
  background: blue;
  border: 1px solid black;
`;

// ✅ BETTER: Use class
element.className = 'styled';

3. Use requestAnimationFrame

// ❌ BAD: Immediate paint
function updateStyles() {
  elements.forEach(el => {
    el.style.background = getComputedColor(el);
  });
}

// ✅ GOOD: Batch in single frame
function updateStyles() {
  requestAnimationFrame(() => {
    elements.forEach(el => {
      el.style.background = getComputedColor(el);
    });
  });
}

Complex Paint Operations

Text Rendering

/* Optimize text rendering */
body {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
}

/* For crisp text on transforms */
.transformed-text {
  backface-visibility: hidden;
}

Images

/* Optimize image rendering */
img {
  image-rendering: auto; /* default */
  /* or */
  image-rendering: crisp-edges; /* sharp pixels */
  /* or */
  image-rendering: pixelated; /* pixelated scaling */
}

Gradients

/* ❌ EXPENSIVE: Complex gradient */
.box {
  background: linear-gradient(
    45deg,
    #ff0000 0%,
    #ff7700 10%,
    #ffff00 20%,
    #00ff00 30%,
    /* ...20 more stops */
  );
}

/* ✅ CHEAPER: Simple gradient */
.box {
  background: linear-gradient(to bottom, #ff0000, #0000ff);
}

/* ✅ CHEAPEST: Solid color */
.box {
  background: #ff0000;
}

Isolating Paint

Layer Promotion

/* Create separate layer */
.video-player {
  will-change: transform;
}

.sticky-header {
  position: fixed;
  will-change: transform;
}

.animated-menu {
  transform: translateZ(0);
}

Paint Boundaries

/* Isolate paint within component */
.component {
  contain: paint;
  isolation: isolate;
}

Debugging Paint Issues

Identify Expensive Paints

// Monitor paint timing
const observer = new PerformanceObserver((list) => {
  list.getEntries().forEach((entry) => {
    if (entry.entryType === 'paint') {
      console.log(entry.name, entry.startTime);
    }
  });
});

observer.observe({ entryTypes: ['paint'] });

Paint Profiling

DevTools → Performance
1. Record
2. Look for "Paint" events
3. Click to see:
   - Paint region
   - Element painted
   - Paint time

Layer Count

// Check layer count
function countLayers() {
  const layers = document.querySelectorAll('[style*="will-change"]');
  console.log('Promoted layers:', layers.length);
  
  // Warning if too many
  if (layers.length > 50) {
    console.warn('Too many layers! May cause memory issues.');
  }
}

CSS Paint API (Houdini)

Custom paint worklet for complex visuals.

// paint-worklet.js
class CirclesPainter {
  paint(ctx, size) {
    const circles = 50;
    for (let i = 0; i < circles; i++) {
      ctx.beginPath();
      ctx.arc(
        Math.random() * size.width,
        Math.random() * size.height,
        Math.random() * 20,
        0,
        2 * Math.PI
      );
      ctx.fillStyle = `rgba(${Math.random() * 255}, 100, 200, 0.5)`;
      ctx.fill();
    }
  }
}

registerPaint('circles', CirclesPainter);
/* Use paint worklet */
.background {
  background-image: paint(circles);
}
// Register worklet
CSS.paintWorklet.addModule('paint-worklet.js');

Best Practices

1. Minimize Paint Regions

Smaller paint area = faster paint
- Use containment
- Isolate changing elements
- Promote to layers

2. Reduce Complexity

Simpler visuals = faster paint
- Avoid complex shadows
- Limit gradient stops
- Use solid colors when possible

3. Layer Management

Balance:
- Too few layers: Expensive repaints
- Too many layers: Memory issues

Sweet spot: 10-20 layers

4. Profile Regularly

Test paint performance:
- Chrome DevTools
- Real devices
- Slow connections
- CPU throttling

Performance Checklist

Use transform/opacity for animations
Promote frequently changing elements to layers
Limit will-change usage
Batch style changes in requestAnimationFrame
Use CSS containment for isolation
Optimize shadows (smaller blur radius)
Avoid :hover on large areas
Monitor layer count (<50)
Profile with Paint Flashing
Test on low-end devices

Common Pitfalls

Animating colors: Repaints every frame
Animate opacity on overlay

Complex shadows everywhere: Slow paint
Simple shadows or no shadows

Too many layers: Memory leak
10-20 layers max

Large paint regions: Slow updates
Contain paint to component

:hover on body: Repaints everything
:hover on specific elements

Optimizing paint is about doing less work—isolate, simplify, and batch operations!

On this page