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!
Problem: Animations can cause:
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.
Key difference:
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:
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:
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:
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}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}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 is a hint for the browser: "Hey, I'm about to animate this property, get ready!"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/* 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}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:
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});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!
Problem: Animating
width or height causes reflow on every frame.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}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}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}Advantages:
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}When to use:
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);Step by step:
1DevTools -> ... (More tools) -> Rendering -> Frame Rendering StatsShows:
1DevTools -> ... (More tools) -> LayersShows:
1/* Check how many elements have will-change */
2*[style*="will-change"] {
3 outline: 2px solid red !important;
4}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}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}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}Properties:
transform and opacitywidth, height, margin, paddingleft, top, right, bottombox-shadow (or animate through pseudo-element)GPU Acceleration:
transform instead of positioningscale instead of width/heightwill-change for frequently animated elementswill-change after the animationOptimization:
requestAnimationFrame, not setIntervalMobile:
prefers-reduced-motionSome 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}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:
Performant patterns:
scale instead of width/heighttranslateX/Y instead of left/topopacity instead of visibility/displayTesting:
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!