Front-end Engineering Lab

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 screen

Why Layers Matter

Without layers:
- Every change repaints entire area
- Slow animations
- Main thread blocking

With layers:
- Independent repainting
- GPU-accelerated
- 60fps animations
- Non-blocking

What 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 scroll

Manual 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 reasons

Enable Layer Borders

DevTools → Rendering → Layer borders
- Blue lines: Layer boundaries
- Orange: Tile boundaries

Layer 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 overhead

Layer 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, bottom

Performance 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 {
  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 elements

FPS Meter

DevTools → Rendering → Frame Rendering Stats
Shows:
- Current FPS
- GPU memory usage
- Frame time

Best Practices Summary

  1. Animate transform/opacity only: 60fps guaranteed
  2. Promote strategically: Only frequently animated elements
  3. Remove will-change after animation
  4. Monitor layer count: <50 on desktop, <30 on mobile
  5. Test on real devices: Especially low-end mobile
  6. Profile with DevTools: Check Layers panel
  7. Balance memory vs performance: More layers ≠ better
  8. Use 3D transforms sparingly: Forces layer creation
  9. Avoid nested promotions: Redundant
  10. 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!

On this page