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
- opacityMeasuring 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 regionsPaint Flashing
DevTools → Rendering → Paint flashing
Green overlay shows repainted regionsLayers Panel
DevTools → Layers (tab)
Shows:
- Composite layers
- Memory usage
- Paint count
- Which elements are on GPUReducing 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 timeLayer 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 layers2. Reduce Complexity
Simpler visuals = faster paint
- Avoid complex shadows
- Limit gradient stops
- Use solid colors when possible3. Layer Management
Balance:
- Too few layers: Expensive repaints
- Too many layers: Memory issues
Sweet spot: 10-20 layers4. Profile Regularly
Test paint performance:
- Chrome DevTools
- Real devices
- Slow connections
- CPU throttlingPerformance 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!