Composite Layers
Leverage GPU acceleration with composite layers for maximum performance
Composite Layers
Composite layers move rendering to the GPU, enabling smooth 60fps animations. Understanding how layers work is key to building performant UIs.
What Are Composite Layers?
The browser creates separate layers for certain elements, which are then composited (combined) by the GPU.
CPU (Main Thread)
├── Parse HTML/CSS
├── JavaScript execution
├── Layout calculation
├── Paint individual layers
└── ⬇️ Send to GPU
GPU (Compositor Thread)
├── Composite layers
└── Display on screenWhy Layers Matter
Without layers:
- Every change repaints entire area
- Slow animations
- Main thread blocking
With layers:
- Independent repainting
- GPU-accelerated
- 60fps animations
- Non-blockingWhat Creates a Layer?
Automatic Layer Promotion
Elements get their own layer when:
1. 3D transforms (translateZ, rotate3d)
2. will-change: transform, opacity
3. `<video>` and `<canvas>` elements
4. CSS filters
5. opacity < 1 with positioned children
6. Fixed positioning
7. Overflow scrollManual Layer Promotion
/* Force layer with translateZ */
.layer {
transform: translateZ(0);
}
/* Or with will-change */
.layer {
will-change: transform;
}
/* Or with backface-visibility */
.layer {
backface-visibility: hidden;
}Transform & Opacity (Compositor-Only)
Only transform and opacity can be animated without repainting.
/* ✅ GOOD: Compositor-only (60fps) */
@keyframes slideIn {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
.animated {
animation: slideIn 0.3s ease-out;
}
/* ❌ BAD: Triggers layout + paint */
@keyframes slideInBad {
from {
left: -100px;
background: red;
}
to {
left: 0;
background: blue;
}
}will-change Property
Hint the browser about upcoming changes.
When to Use
/* Element will animate */
.menu {
will-change: transform;
}
/* Remove after animation */
.menu.animated {
will-change: auto;
}Don't Overuse
/* ❌ BAD: Everything has will-change */
* {
will-change: transform, opacity;
}
/* ✅ GOOD: Only animated elements */
.modal {
will-change: transform;
}
.dropdown {
will-change: transform;
}Dynamic will-change
function AnimatedComponent() {
const [animating, setAnimating] = useState(false);
return (
<div
style={{
willChange: animating ? 'transform' : 'auto',
transform: animating ? 'scale(1.2)' : 'scale(1)',
}}
onMouseEnter={() => setAnimating(true)}
onMouseLeave={() => setAnimating(false)}
>
Hover me
</div>
);
}Viewing Layers
Chrome DevTools
1. Open DevTools
2. Layers tab
3. Click elements to see:
- Layer count
- Memory usage
- Paint count
- Compositing reasonsEnable Layer Borders
DevTools → Rendering → Layer borders
- Blue lines: Layer boundaries
- Orange: Tile boundariesLayer Optimization Strategies
1. Promote Animated Elements
/* Elements that animate frequently */
.video-player {
will-change: transform;
}
.loading-spinner {
will-change: transform;
}
.modal-overlay {
will-change: opacity;
}2. Isolate Expensive Paints
/* Separate layer for complex shadow */
.card::after {
content: '';
position: absolute;
inset: -10px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
transform: translateZ(0);
z-index: -1;
}3. Fixed Positioning
/* Fixed elements automatically get layers */
.sticky-header {
position: fixed;
top: 0;
/* Already on separate layer */
}4. Scrolling Containers
/* Create layer for smooth scrolling */
.scroll-container {
overflow-y: scroll;
will-change: scroll-position;
}Layer Memory Cost
Each layer uses GPU memory. Too many = performance issues.
Measuring Memory Usage
// Check layer count
function checkLayers() {
const promotedElements = document.querySelectorAll('[style*="will-change"]');
console.log('Manual layers:', promotedElements.length);
// Warning threshold
if (promotedElements.length > 50) {
console.warn('Too many layers! Reduce will-change usage.');
}
}
// Monitor memory
if (performance.memory) {
console.log('Used JS Heap:', (performance.memory.usedJSHeapSize / 1048576).toFixed(2), 'MB');
console.log('Total JS Heap:', (performance.memory.totalJSHeapSize / 1048576).toFixed(2), 'MB');
}Layer Budget
Recommended limits:
- Desktop: 50-100 layers max
- Mobile: 20-30 layers max
- Each layer: ~100KB-1MB memory
Balance:
- Too few: Expensive repaints
- Too many: Memory issues, GPU overheadLayer Anti-Patterns
❌ Promoting Everything
/* BAD: Everything on GPU */
* {
transform: translateZ(0);
}❌ Nested Layers
/* BAD: Unnecessary nesting */
.parent {
will-change: transform;
}
.child {
will-change: transform; /* Redundant */
}❌ Permanent will-change
/* BAD: Always hints GPU */
.element {
will-change: transform;
}
/* GOOD: Only during animation */
.element.animating {
will-change: transform;
}3D Transforms
Force GPU acceleration with 3D context.
/* Force 3D context */
.accelerated {
transform: translate3d(0, 0, 0);
/* or */
transform: translateZ(0);
}
/* Smooth 3D transforms */
.card {
transform-style: preserve-3d;
backface-visibility: hidden;
}
/* 3D flip animation */
@keyframes flip {
from {
transform: rotateY(0deg);
}
to {
transform: rotateY(180deg);
}
}
.flip-card {
animation: flip 0.6s ease-in-out;
transform-style: preserve-3d;
}Composite Layer Best Practices
1. Promote Strategically
Promote:
✅ Frequently animated elements
✅ Fixed/sticky positioning
✅ Video/canvas elements
✅ Scrolling containers
Don't Promote:
❌ Static content
❌ Rarely changing elements
❌ Everything "just in case"2. Remove After Animation
function Modal({ isOpen }: Props) {
return (
<div
style={{
willChange: isOpen ? 'transform, opacity' : 'auto',
transform: isOpen ? 'scale(1)' : 'scale(0.9)',
opacity: isOpen ? 1 : 0,
}}
>
Modal content
</div>
);
}3. Monitor Memory
// Check if too many layers
function auditLayers() {
const layers = {
willChange: document.querySelectorAll('[style*="will-change"]').length,
translateZ: document.querySelectorAll('[style*="translateZ"]').length,
fixed: document.querySelectorAll('[style*="position: fixed"]').length,
};
console.table(layers);
const total = Object.values(layers).reduce((a, b) => a + b, 0);
if (total > 50) {
console.warn('Too many layers:', total);
}
}4. Use Appropriate Properties
Compositor-only (cheapest):
- transform: translate, scale, rotate
- opacity
Paint (medium cost):
- color, background-color
- box-shadow, text-shadow
Layout (most expensive):
- width, height
- padding, margin
- top, left, right, bottomPerformance Patterns
Smooth Hover Effects
.button {
transition: transform 0.2s ease-out;
will-change: transform;
}
.button:hover {
transform: scale(1.05);
}Loading Spinner
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.spinner {
animation: spin 1s linear infinite;
will-change: transform;
}Modal Animation
.modal {
transition: transform 0.3s, opacity 0.3s;
will-change: transform, opacity;
}
.modal.visible {
transform: scale(1);
opacity: 1;
}
.modal.hidden {
transform: scale(0.9);
opacity: 0;
}Parallax Scrolling
function ParallaxBackground() {
const [offsetY, setOffsetY] = useState(0);
useEffect(() => {
const handleScroll = () => {
requestAnimationFrame(() => {
setOffsetY(window.scrollY * 0.5);
});
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return (
<div
style={{
transform: `translateY(${offsetY}px)`,
willChange: 'transform',
}}
>
Background image
</div>
);
}Debugging Layer Issues
Check Compositing Reasons
DevTools → Layers tab → Select layer
Shows why element was promoted:
- "has a will-change hint for a property"
- "has a 3D transform"
- "is a fixed position element"
- "has a CSS animation"Paint Flashing
DevTools → Rendering → Paint flashing
- Green: Repainted area
- Should be minimal for animated elementsFPS Meter
DevTools → Rendering → Frame Rendering Stats
Shows:
- Current FPS
- GPU memory usage
- Frame timeBest Practices Summary
- Animate transform/opacity only: 60fps guaranteed
- Promote strategically: Only frequently animated elements
- Remove will-change after animation
- Monitor layer count:
<50on desktop,<30on mobile - Test on real devices: Especially low-end mobile
- Profile with DevTools: Check Layers panel
- Balance memory vs performance: More layers ≠ better
- Use 3D transforms sparingly: Forces layer creation
- Avoid nested promotions: Redundant
- Document layer decisions: Comment why elements are promoted
Common Pitfalls
❌ Promoting everything: Memory bloat
✅ Promote only animated elements
❌ Permanent will-change: Wastes GPU memory
✅ Add/remove dynamically
❌ Nested layers: Redundant
✅ Promote parent OR children
❌ Animating non-compositor properties: Jank
✅ Transform/opacity only
❌ No testing on mobile: Memory issues
✅ Test on target devices
Composite layers are powerful but expensive—use them wisely for smooth, performant animations!