We use cookies to enhance your experience on the site
CodeWorlds

CSS Animation Performance - optimization and GPU acceleration

Welcome to the Pyramid Builders' Workshop! Just as the ancient Egyptians had to build pyramids efficiently to save resources and time, we must create performant animations that run smoothly even on weaker devices. Learn the secrets of GPU acceleration, will-change, and how to avoid slow animations!

Why does animation performance matter?

Problem: Animations can cause:

  • FPS drop (frames per second) below 60 FPS
  • Lagging and "stuttering"
  • Battery drain on mobile devices
  • Poor user experience (UX)

Goal: Smooth 60 FPS = 16.67ms per frame

Egyptian analogy: Just as pyramid builders had to use the right techniques (levers, ramps) to lift heavy blocks, we must use the right CSS properties so that animations are light and fast.

GPU vs CPU - understanding rendering

CPU (Central Processing Unit)

  • The main computer processor
  • General purpose
  • Slower for graphics

GPU (Graphics Processing Unit)

  • The graphics processor
  • Optimized for graphical calculations
  • Much faster for animations and transformations

Key difference:

  • CPU: Sequential processing
  • GPU: Parallel processing of thousands of pixels simultaneously

GPU-accelerated (FAST) vs CPU-bound (SLOW) properties

GPU-accelerated (ALWAYS use for animations)

1/* Transform - all functions */
2.fast {
3  transform: translate(100px, 50px);    /* GPU */
4  transform: scale(1.5);                /* GPU */
5  transform: rotate(45deg);             /* GPU */
6  transform: skew(10deg);               /* GPU */
7  transform: translateZ(0);             /* GPU */
8  transform: translate3d(10px, 20px, 0);/* GPU */
9}
10
11/* Opacity */
12.fast {
13  opacity: 0.5;                         /* GPU */
14}

Why they're fast:

  • They don't cause reflow (layout recalculation)
  • They don't cause repaint (repainting)
  • GPU renders them in a separate layer (compositing layer)

CPU-bound (AVOID in animations)

1/* Properties that change layout */
2.slow {
3  width: 300px;        /* CPU - reflow of entire page */
4  height: 200px;       /* CPU - reflow of entire page */
5  margin: 20px;        /* CPU - reflow of entire page */
6  padding: 10px;       /* CPU - reflow of entire page */
7  top: 100px;          /* CPU - element reflow */
8  left: 50px;          /* CPU - element reflow */
9  font-size: 20px;     /* CPU - text reflow */
10}
11
12/* Properties that change appearance */
13.slow {
14  background: red;     /* CPU - repaint */
15  color: blue;         /* CPU - repaint */
16  border: 1px solid;   /* CPU - repaint */
17  box-shadow: 0 0 10px;/* CPU - repaint (heavy!) */
18}

Why they're slow:

  • Reflow = the browser must recalculate positions of ALL elements
  • Repaint = the browser must repaint pixels
  • They block the main thread

Practical examples - SLOW vs FAST

Example 1: Moving an element

1/* SLOW - animating left */
2.box-slow {
3  position: relative;
4  left: 0;
5  transition: left 0.3s;
6}
7
8.box-slow:hover {
9  left: 100px;  /* Causes reflow on every frame! */
10}
11
12/* FAST - animating transform */
13.box-fast {
14  transform: translateX(0);
15  transition: transform 0.3s;
16}
17
18.box-fast:hover {
19  transform: translateX(100px);  /* GPU-accelerated! */
20}

Result:

  • Slow: ~30-40 FPS (visible lagging)
  • Fast: 60 FPS (smooth)

Example 2: Enlarging an element

1/* SLOW - animating width/height */
2.card-slow {
3  width: 200px;
4  height: 300px;
5  transition: width 0.3s, height 0.3s;
6}
7
8.card-slow:hover {
9  width: 220px;    /* Reflow! */
10  height: 330px;   /* Reflow! */
11}
12
13/* FAST - animating scale */
14.card-fast {
15  width: 200px;
16  height: 300px;
17  transform: scale(1);
18  transition: transform 0.3s;
19}
20
21.card-fast:hover {
22  transform: scale(1.1);  /* GPU! 220px x 330px */
23}

Example 3: Fade in/out

1/* SLOW - animating visibility */
2.modal-slow {
3  visibility: visible;
4  transition: visibility 0.3s;
5}
6
7.modal-slow.hidden {
8  visibility: hidden;  /* Cannot be animated! Jump! */
9}
10
11/* FAST - animating opacity */
12.modal-fast {
13  opacity: 1;
14  transition: opacity 0.3s;
15}
16
17.modal-fast.hidden {
18  opacity: 0;  /* Smooth GPU animation! */
19  pointer-events: none;  /* Disable interactions */
20}

Example 4: Box shadow (very expensive!)

1/* VERY SLOW - animating box-shadow */
2.button-slow {
3  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
4  transition: box-shadow 0.3s;
5}
6
7.button-slow:hover {
8  box-shadow: 0 8px 16px rgba(0,0,0,0.3);  /* Very heavy! */
9}
10
11/* FASTER - pseudo-element with opacity */
12.button-fast {
13  position: relative;
14}
15
16.button-fast::after {
17  content: '';
18  position: absolute;
19  inset: 0;
20  box-shadow: 0 8px 16px rgba(0,0,0,0.3);
21  opacity: 0;
22  transition: opacity 0.3s;
23  pointer-events: none;
24}
25
26.button-fast:hover::after {
27  opacity: 1;  /* Animate only opacity! */
28}

will-change - notifying the browser about future changes

will-change
is a hint for the browser: "Hey, I'm about to animate this property, get ready!"

How will-change works

1.element {
2  will-change: transform;
3  /* Browser creates a GPU layer BEFORE the animation */
4}
5
6.element:hover {
7  transform: scale(1.2);  /* Already ready! */
8}

What happens:

  1. The browser creates a separate layer (compositing layer) for the element
  2. The element is rendered by the GPU
  3. The animation is instant and smooth

When to use will-change

1/* GOOD - for frequently animated elements */
2.menu-item {
3  will-change: transform;
4  transition: transform 0.2s;
5}
6
7.menu-item:hover {
8  transform: translateY(-5px);
9}
10
11/* GOOD - for keyframe animations */
12.spinner {
13  will-change: transform;
14  animation: spin 1s linear infinite;
15}
16
17@keyframes spin {
18  to { transform: rotate(360deg); }
19}
20
21/* GOOD - add dynamically before animation */
22.modal {
23  /* No will-change by default */
24}
25
26.modal.opening {
27  will-change: transform, opacity;  /* Add before animation */
28}
29
30.modal.open {
31  will-change: auto;  /* Remove after animation! */
32}

When NOT to use will-change

1/* BAD - on all elements */
2* {
3  will-change: transform;  /* Uses LOTS of memory! */
4}
5
6/* BAD - on static elements */
7.logo {
8  will-change: transform;  /* The logo doesn't move! */
9}
10
11/* BAD - too many properties */
12.element {
13  will-change: transform, opacity, width, height, background, color;
14  /* Too much - counterproductive! */
15}
16
17/* BAD - left permanently */
18.button {
19  will-change: transform;  /* Left forever = memory leak */
20}

will-change rules:

  1. Only for animated elements
  2. Only 1-2 properties at a time
  3. Add before the animation, remove after the animation
  4. Test memory usage - can be counterproductive!

Dynamically adding will-change (JavaScript)

1const element = document.querySelector('.box');
2
3// Add before animation (e.g., on mouseenter)
4element.addEventListener('mouseenter', () => {
5  element.style.willChange = 'transform';
6});
7
8// Remove after animation (on transitionend)
9element.addEventListener('transitionend', () => {
10  element.style.willChange = 'auto';
11});

Force GPU acceleration - the translateZ(0) trick

If the browser doesn't create a GPU layer automatically, you can force it:

1.element {
2  /* Force GPU layer */
3  transform: translateZ(0);
4  /* or */
5  transform: translate3d(0, 0, 0);
6  /* or */
7  will-change: transform;
8}

Practical example:

1/* Image that will be zoomed */
2.image {
3  transform: translateZ(0);  /* Force GPU */
4  transition: transform 0.3s;
5}
6
7.image:hover {
8  transform: scale(1.1);  /* Now guaranteed GPU */
9}

Warning: Don't use on all elements - it uses GPU memory!

Avoiding width/height animation - layout thrashing

Problem: Animating

width
or
height
causes reflow on every frame.

Slow way - width animation

1.progress-bar-slow {
2  width: 0%;
3  transition: width 2s;
4}
5
6.progress-bar-slow.complete {
7  width: 100%;  /* Reflow on every frame! */
8}

Fast way - transform: scaleX

1.progress-bar-fast {
2  width: 100%;  /* Full width immediately */
3  transform: scaleX(0);  /* Scale 0% */
4  transform-origin: left;  /* Grow from the left */
5  transition: transform 2s;
6}
7
8.progress-bar-fast.complete {
9  transform: scaleX(1);  /* Scale 100% - GPU! */
10}

Alternative - clip-path

1.progress-bar-clip {
2  width: 100%;
3  background: linear-gradient(to right, #3498db, #2ecc71);
4  clip-path: inset(0 100% 0 0);  /* Hide the right side */
5  transition: clip-path 2s;
6}
7
8.progress-bar-clip.complete {
9  clip-path: inset(0 0 0 0);  /* Show everything */
10}

requestAnimationFrame vs CSS Animations

CSS Animations - PREFERRED

Advantages:

  • Optimized by the browser
  • Run in a separate thread (compositor thread)
  • Smooth even when the main thread is busy
  • Less code
  • Automatic GPU acceleration
1/* CSS Animation - preferred */
2.box {
3  animation: slide 1s ease-in-out infinite;
4}
5
6@keyframes slide {
7  0% { transform: translateX(0); }
8  100% { transform: translateX(100px); }
9}

requestAnimationFrame (rAF) - for complex animations

When to use:

  • Animations dependent on user interaction (scroll, mouse)
  • Animations with dynamic values (physics, easing)
  • Synchronization of multiple animations
  • Canvas/WebGL animations
1/* JavaScript rAF - for complex cases */
2let position = 0;
3const box = document.querySelector('.box');
4
5function animate() {
6  position += 2;
7
8  // Use transform, not left!
9  box.style.transform = `translateX(${position}px)`;
10
11  if (position < 500) {
12    requestAnimationFrame(animate);
13  }
14}
15
16requestAnimationFrame(animate);

Do NOT use setInterval/setTimeout:

1/* BAD - setInterval */
2setInterval(() => {
3  position += 2;
4  box.style.left = position + 'px';  /* Not synchronized with screen! */
5}, 16);
6
7/* GOOD - requestAnimationFrame */
8function animate() {
9  position += 2;
10  box.style.transform = `translateX(${position}px)`;
11  requestAnimationFrame(animate);
12}
13requestAnimationFrame(animate);

Performance monitoring - how to measure performance

1. Chrome DevTools Performance Tab

Step by step:

  1. Open DevTools (F12)
  2. Performance tab
  3. Click Record (red circle)
  4. Perform the animation
  5. Click Stop
  6. Analyze:
    • FPS - is it 60 FPS?
    • CPU - is the main thread busy?
    • Rendering - how much time on reflow/repaint?

2. Rendering Tab - FPS Meter

1DevTools -> ... (More tools) -> Rendering -> Frame Rendering Stats

Shows:

  • FPS in real time
  • GPU memory usage
  • Paint flashing (red rectangles = repaint)

3. Layers Panel

1DevTools -> ... (More tools) -> Layers

Shows:

  • Which elements have their own GPU layer
  • Why the layer was created
  • How much memory it uses

4. CSS Stats - will-change monitoring

1/* Check how many elements have will-change */
2*[style*="will-change"] {
3  outline: 2px solid red !important;
4}

Optimization examples - before and after

Optimization 1: Card hover effect

1/* BEFORE - slow (4 repaints, 2 reflows) */
2.card-before {
3  transition: all 0.3s;
4}
5
6.card-before:hover {
7  width: 320px;         /* Reflow */
8  box-shadow: 0 10px 30px rgba(0,0,0,0.3);  /* Repaint */
9  border: 2px solid gold;  /* Repaint */
10}
11
12/* AFTER - fast (0 reflows, 1 composite) */
13.card-after {
14  position: relative;
15  transition: transform 0.3s;
16}
17
18.card-after::before {
19  content: '';
20  position: absolute;
21  inset: -2px;
22  border: 2px solid gold;
23  opacity: 0;
24  transition: opacity 0.3s;
25}
26
27.card-after::after {
28  content: '';
29  position: absolute;
30  inset: 0;
31  box-shadow: 0 10px 30px rgba(0,0,0,0.3);
32  opacity: 0;
33  transition: opacity 0.3s;
34}
35
36.card-after:hover {
37  transform: scale(1.05);  /* GPU */
38}
39
40.card-after:hover::before,
41.card-after:hover::after {
42  opacity: 1;  /* GPU */
43}

Optimization 2: Loading spinner

1/* BEFORE - slow (border animation) */
2.spinner-before {
3  border: 4px solid #f3f3f3;
4  border-top: 4px solid #3498db;
5  border-radius: 50%;
6  animation: spin-slow 1s linear infinite;
7}
8
9@keyframes spin-slow {
10  to {
11    transform: rotate(360deg);
12    border-top-color: #e74c3c;  /* Color animation = repaint! */
13  }
14}
15
16/* AFTER - fast (only transform) */
17.spinner-after {
18  border: 4px solid #f3f3f3;
19  border-top: 4px solid #3498db;
20  border-radius: 50%;
21  will-change: transform;
22  animation: spin-fast 1s linear infinite;
23}
24
25@keyframes spin-fast {
26  to {
27    transform: rotate(360deg);  /* GPU only! */
28  }
29}

Optimization 3: Smooth scroll reveal

1/* BEFORE - slow (top + opacity animation) */
2.reveal-before {
3  position: relative;
4  top: 50px;
5  opacity: 0;
6  transition: top 0.6s, opacity 0.6s;
7}
8
9.reveal-before.visible {
10  top: 0;       /* Reflow */
11  opacity: 1;   /* GPU */
12}
13
14/* AFTER - fast (only transform + opacity) */
15.reveal-after {
16  transform: translateY(50px);
17  opacity: 0;
18  will-change: transform, opacity;
19  transition: transform 0.6s, opacity 0.6s;
20}
21
22.reveal-after.visible {
23  transform: translateY(0);  /* GPU */
24  opacity: 1;                /* GPU */
25}

Animation performance checklist

Properties:

  • I only animate
    transform
    and
    opacity
  • I avoid
    width
    ,
    height
    ,
    margin
    ,
    padding
  • I avoid
    left
    ,
    top
    ,
    right
    ,
    bottom
  • I avoid
    box-shadow
    (or animate through pseudo-element)

GPU Acceleration:

  • I use
    transform
    instead of positioning
  • I use
    scale
    instead of
    width/height
  • I add
    will-change
    for frequently animated elements
  • I remove
    will-change
    after the animation

Optimization:

  • I prefer CSS animations over JavaScript
  • If JS - I use
    requestAnimationFrame
    , not
    setInterval
  • I test in Chrome DevTools Performance
  • I check FPS Meter (goal: 60 FPS)
  • I check Layers Panel (memory usage)

Mobile:

  • I test on real mobile devices
  • I limit the number of simultaneous animations
  • I disable animations for
    prefers-reduced-motion

Prefers-reduced-motion - accessibility

Some users have the "Reduce motion" setting enabled in their system (dizziness, epilepsy). Let's respect that:

1/* Normal animations */
2.box {
3  transition: transform 0.3s;
4}
5
6.box:hover {
7  transform: scale(1.2) rotate(5deg);
8}
9
10/* Disable for users with prefers-reduced-motion */
11@media (prefers-reduced-motion: reduce) {
12  .box {
13    transition: none;  /* No animation */
14  }
15
16  .box:hover {
17    transform: scale(1.05);  /* Only subtle change */
18  }
19}
20
21/* Or globally */
22@media (prefers-reduced-motion: reduce) {
23  *,
24  *::before,
25  *::after {
26    animation-duration: 0.01ms !important;
27    animation-iteration-count: 1 !important;
28    transition-duration: 0.01ms !important;
29  }
30}

Summary

GPU-accelerated (FAST):

  • transform
    (translate, scale, rotate, skew)
  • opacity
  • filter
    (blur, brightness - but slower than transform)

CPU-bound (SLOW):

  • width
    ,
    height
    ,
    margin
    ,
    padding
  • left
    ,
    top
    ,
    right
    ,
    bottom
  • font-size
    ,
    border-width
  • box-shadow
    (very slow!)
  • background-color
    ,
    color
    (repaint)

will-change:

  • Use sparingly (1-2 properties)
  • Add before the animation, remove after
  • Don't leave permanently (memory leak!)

Performant patterns:

  • Use
    scale
    instead of
    width/height
  • Use
    translateX/Y
    instead of
    left/top
  • Use
    opacity
    instead of
    visibility/display
  • Animate shadows through pseudo-elements with opacity

Testing:

  • Chrome DevTools Performance Tab
  • FPS Meter (60 FPS = goal)
  • Layers Panel (GPU memory)
  • Test on mobile devices

Remember: Efficient animations are like a well-organized pyramid construction - use the right tools (GPU = levers) instead of brute force (CPU = manual lifting), and your page will be as smooth as sand in the desert!

Go to CodeWorlds