HTML and CSS course ยท Module 6: CSS Animations
CSS Animation Performance - optimization and GPU acceleration
In this lesson13
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)
/* Transform - all functions */
.fast {
transform: translate(100px, 50px); /* GPU */
transform: scale(1.5); /* GPU */
transform: rotate(45deg); /* GPU */
transform: skew(10deg); /* GPU */
transform: translateZ(0); /* GPU */
transform: translate3d(10px, 20px, 0);/* GPU */
}
/* Opacity */
.fast {
opacity: 0.5; /* GPU */
}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)
/* Properties that change layout */
.slow {
width: 300px; /* CPU - reflow of entire page */
height: 200px; /* CPU - reflow of entire page */
margin: 20px; /* CPU - reflow of entire page */
padding: 10px; /* CPU - reflow of entire page */
top: 100px; /* CPU - element reflow */
left: 50px; /* CPU - element reflow */
font-size: 20px; /* CPU - text reflow */
}
/* Properties that change appearance */
.slow {
background: red; /* CPU - repaint */
color: blue; /* CPU - repaint */
border: 1px solid; /* CPU - repaint */
box-shadow: 0 0 10px;/* CPU - repaint (heavy!) */
}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
/* SLOW - animating left */
.box-slow {
position: relative;
left: 0;
transition: left 0.3s;
}
.box-slow:hover {
left: 100px; /* Causes reflow on every frame! */
}
/* FAST - animating transform */
.box-fast {
transform: translateX(0);
transition: transform 0.3s;
}
.box-fast:hover {
transform: translateX(100px); /* GPU-accelerated! */
}Result:
- Slow: ~30-40 FPS (visible lagging)
- Fast: 60 FPS (smooth)
Example 2: Enlarging an element
/* SLOW - animating width/height */
.card-slow {
width: 200px;
height: 300px;
transition: width 0.3s, height 0.3s;
}
.card-slow:hover {
width: 220px; /* Reflow! */
height: 330px; /* Reflow! */
}
/* FAST - animating scale */
.card-fast {
width: 200px;
height: 300px;
transform: scale(1);
transition: transform 0.3s;
}
.card-fast:hover {
transform: scale(1.1); /* GPU! 220px x 330px */
}Example 3: Fade in/out
/* SLOW - animating visibility */
.modal-slow {
visibility: visible;
transition: visibility 0.3s;
}
.modal-slow.hidden {
visibility: hidden; /* Cannot be animated! Jump! */
}
/* FAST - animating opacity */
.modal-fast {
opacity: 1;
transition: opacity 0.3s;
}
.modal-fast.hidden {
opacity: 0; /* Smooth GPU animation! */
pointer-events: none; /* Disable interactions */
}Example 4: Box shadow (very expensive!)
/* VERY SLOW - animating box-shadow */
.button-slow {
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: box-shadow 0.3s;
}
.button-slow:hover {
box-shadow: 0 8px 16px rgba(0,0,0,0.3); /* Very heavy! */
}
/* FASTER - pseudo-element with opacity */
.button-fast {
position: relative;
}
.button-fast::after {
content: '';
position: absolute;
inset: 0;
box-shadow: 0 8px 16px rgba(0,0,0,0.3);
opacity: 0;
transition: opacity 0.3s;
pointer-events: none;
}
.button-fast:hover::after {
opacity: 1; /* Animate only opacity! */
}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
.element {
will-change: transform;
/* Browser creates a GPU layer BEFORE the animation */
}
.element:hover {
transform: scale(1.2); /* Already ready! */
}What happens:
- The browser creates a separate layer (compositing layer) for the element
- The element is rendered by the GPU
- The animation is instant and smooth
When to use will-change
/* GOOD - for frequently animated elements */
.menu-item {
will-change: transform;
transition: transform 0.2s;
}
.menu-item:hover {
transform: translateY(-5px);
}
/* GOOD - for keyframe animations */
.spinner {
will-change: transform;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* GOOD - add dynamically before animation */
.modal {
/* No will-change by default */
}
.modal.opening {
will-change: transform, opacity; /* Add before animation */
}
.modal.open {
will-change: auto; /* Remove after animation! */
}When NOT to use will-change
/* BAD - on all elements */
* {
will-change: transform; /* Uses LOTS of memory! */
}
/* BAD - on static elements */
.logo {
will-change: transform; /* The logo doesn't move! */
}
/* BAD - too many properties */
.element {
will-change: transform, opacity, width, height, background, color;
/* Too much - counterproductive! */
}
/* BAD - left permanently */
.button {
will-change: transform; /* Left forever = memory leak */
}will-change rules:
- Only for animated elements
- Only 1-2 properties at a time
- Add before the animation, remove after the animation
- Test memory usage - can be counterproductive!
Dynamically adding will-change (JavaScript)
const element = document.querySelector('.box');
// Add before animation (e.g., on mouseenter)
element.addEventListener('mouseenter', () => {
element.style.willChange = 'transform';
});
// Remove after animation (on transitionend)
element.addEventListener('transitionend', () => {
element.style.willChange = 'auto';
});Force GPU acceleration - the translateZ(0) trick
If the browser doesn't create a GPU layer automatically, you can force it:
.element {
/* Force GPU layer */
transform: translateZ(0);
/* or */
transform: translate3d(0, 0, 0);
/* or */
will-change: transform;
}Practical example:
/* Image that will be zoomed */
.image {
transform: translateZ(0); /* Force GPU */
transition: transform 0.3s;
}
.image:hover {
transform: scale(1.1); /* Now guaranteed GPU */
}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
.progress-bar-slow {
width: 0%;
transition: width 2s;
}
.progress-bar-slow.complete {
width: 100%; /* Reflow on every frame! */
}Fast way - transform: scaleX
.progress-bar-fast {
width: 100%; /* Full width immediately */
transform: scaleX(0); /* Scale 0% */
transform-origin: left; /* Grow from the left */
transition: transform 2s;
}
.progress-bar-fast.complete {
transform: scaleX(1); /* Scale 100% - GPU! */
}Alternative - clip-path
.progress-bar-clip {
width: 100%;
background: linear-gradient(to right, #3498db, #2ecc71);
clip-path: inset(0 100% 0 0); /* Hide the right side */
transition: clip-path 2s;
}
.progress-bar-clip.complete {
clip-path: inset(0 0 0 0); /* Show everything */
}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
/* CSS Animation - preferred */
.box {
animation: slide 1s ease-in-out infinite;
}
@keyframes slide {
0% { transform: translateX(0); }
100% { transform: translateX(100px); }
}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
/* JavaScript rAF - for complex cases */
let position = 0;
const box = document.querySelector('.box');
function animate() {
position += 2;
// Use transform, not left!
box.style.transform = `translateX(${position}px)`;
if (position < 500) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);Do NOT use setInterval/setTimeout:
/* BAD - setInterval */
setInterval(() => {
position += 2;
box.style.left = position + 'px'; /* Not synchronized with screen! */
}, 16);
/* GOOD - requestAnimationFrame */
function animate() {
position += 2;
box.style.transform = `translateX(${position}px)`;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);Performance monitoring - how to measure performance
1. Chrome DevTools Performance Tab
Step by step:
- Open DevTools (F12)
- Performance tab
- Click Record (red circle)
- Perform the animation
- Click Stop
- Analyze:
- FPS - is it 60 FPS?
- CPU - is the main thread busy?
- Rendering - how much time on reflow/repaint?
2. Rendering Tab - FPS Meter
DevTools -> ... (More tools) -> Rendering -> Frame Rendering StatsShows:
- FPS in real time
- GPU memory usage
- Paint flashing (red rectangles = repaint)
3. Layers Panel
DevTools -> ... (More tools) -> LayersShows:
- Which elements have their own GPU layer
- Why the layer was created
- How much memory it uses
4. CSS Stats - will-change monitoring
/* Check how many elements have will-change */
*[style*="will-change"] {
outline: 2px solid red !important;
}Optimization examples - before and after
Optimization 1: Card hover effect
/* BEFORE - slow (4 repaints, 2 reflows) */
.card-before {
transition: all 0.3s;
}
.card-before:hover {
width: 320px; /* Reflow */
box-shadow: 0 10px 30px rgba(0,0,0,0.3); /* Repaint */
border: 2px solid gold; /* Repaint */
}
/* AFTER - fast (0 reflows, 1 composite) */
.card-after {
position: relative;
transition: transform 0.3s;
}
.card-after::before {
content: '';
position: absolute;
inset: -2px;
border: 2px solid gold;
opacity: 0;
transition: opacity 0.3s;
}
.card-after::after {
content: '';
position: absolute;
inset: 0;
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
opacity: 0;
transition: opacity 0.3s;
}
.card-after:hover {
transform: scale(1.05); /* GPU */
}
.card-after:hover::before,
.card-after:hover::after {
opacity: 1; /* GPU */
}Optimization 2: Loading spinner
/* BEFORE - slow (border animation) */
.spinner-before {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin-slow 1s linear infinite;
}
@keyframes spin-slow {
to {
transform: rotate(360deg);
border-top-color: #e74c3c; /* Color animation = repaint! */
}
}
/* AFTER - fast (only transform) */
.spinner-after {
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
will-change: transform;
animation: spin-fast 1s linear infinite;
}
@keyframes spin-fast {
to {
transform: rotate(360deg); /* GPU only! */
}
}Optimization 3: Smooth scroll reveal
/* BEFORE - slow (top + opacity animation) */
.reveal-before {
position: relative;
top: 50px;
opacity: 0;
transition: top 0.6s, opacity 0.6s;
}
.reveal-before.visible {
top: 0; /* Reflow */
opacity: 1; /* GPU */
}
/* AFTER - fast (only transform + opacity) */
.reveal-after {
transform: translateY(50px);
opacity: 0;
will-change: transform, opacity;
transition: transform 0.6s, opacity 0.6s;
}
.reveal-after.visible {
transform: translateY(0); /* GPU */
opacity: 1; /* GPU */
}Animation performance checklist
Properties:
- I only animate
transformandopacity - 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
transforminstead of positioning - I use
scaleinstead ofwidth/height - I add
will-changefor frequently animated elements - I remove
will-changeafter the animation
Optimization:
- I prefer CSS animations over JavaScript
- If JS - I use
requestAnimationFrame, notsetInterval - 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:
/* Normal animations */
.box {
transition: transform 0.3s;
}
.box:hover {
transform: scale(1.2) rotate(5deg);
}
/* Disable for users with prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
.box {
transition: none; /* No animation */
}
.box:hover {
transform: scale(1.05); /* Only subtle change */
}
}
/* Or globally */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}Summary
GPU-accelerated (FAST):
transform(translate, scale, rotate, skew)opacityfilter(blur, brightness - but slower than transform)
CPU-bound (SLOW):
width,height,margin,paddingleft,top,right,bottomfont-size,border-widthbox-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
scaleinstead ofwidth/height - Use
translateX/Yinstead ofleft/top - Use
opacityinstead ofvisibility/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!
Code for this lesson: style.css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', sans-serif;
background: linear-gradient(180deg, #ff6b35, #ff9a56, #ffd700);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
color: white;
}
.scene {
text-align: center;
padding: 20px;
}
h2 {
margin-bottom: 30px;
color: #3d2914;
}
.sun-container {
position: relative;
width: 300px;
height: 300px;
margin: 0 auto;
}
.sun {
font-size: 80px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/* Add the pulseSun animation */
/* animation: pulseSun 2s ease-in-out infinite; */
}
.ray {
position: absolute;
width: 4px;
height: 60px;
background: linear-gradient(180deg, #ffd700, transparent);
top: 50%;
left: 50%;
transform-origin: center bottom;
opacity: 0;
/* Add the rayGlow animation */
/* animation: rayGlow 3s ease-in-out infinite; */
}
.ray-1 { transform: translate(-50%, -100%) rotate(0deg); }
.ray-2 { transform: translate(-50%, -100%) rotate(90deg); }
.ray-3 { transform: translate(-50%, -100%) rotate(180deg); }
.ray-4 { transform: translate(-50%, -100%) rotate(270deg); }
/* Add different animation-delay values */
/* .ray-1 { animation-delay: 0s; } */
/* .ray-2 { animation-delay: 0.5s; } */
/* .ray-3 { animation-delay: 1s; } */
/* .ray-4 { animation-delay: 1.5s; } */
/* Define @keyframes pulseSun */
/* @keyframes pulseSun { */
/* 0%, 100% { transform: translate(-50%, -50%) scale(1); } */
/* 50% { transform: translate(-50%, -50%) scale(1.2); } */
/* } */
/* Define @keyframes rayGlow */
/* @keyframes rayGlow { */
/* 0% { opacity: 0; height: 30px; } */
/* 50% { opacity: 1; height: 80px; } */
/* 100% { opacity: 0; height: 30px; } */
/* } */
.hint {
margin-top: 30px;
color: #3d2914;
font-style: italic;
}Docs entries used in this lesson
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Which CSS properties are GPU-accelerated and best for smooth animations?
2. Which frames-per-second (FPS) value means a smooth animation?
These are 2 of 4 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Vertical ordering
Order these ways of animating from most performant (GPU) to slowest (reflow):