PageSourceSearch

https://www.siwalik.in/posts/interactive-guide-to-web-animation-performance/

html siwalik.in collected 2026-09-24 10:39:04 UTC 108,198 bytes, 543 lines download raw bytes

1<!DOCTYPE html><html lang="en" class="scroll-smooth"> <head><meta name="astro-view-transitions-enabled" content="true"><meta name="astro-view-transitions-fallback" content="animate"><title>An Interactive Guide to Web Animation Performance • Siwalik Mukherjee</title><meta charset="UTF-8"><link rel="canonical" href="https://www.siwalik.in/posts/interactive-guide-to-web-animation-performance/"><meta name="description" content="Why does your animation jank on Android but not your MacBook? An interactive guide to how browsers render frames, and the techniques to fix it — FLIP, GPU layers, scroll-driven CSS, and more."><meta name="robots" content="index, follow"><meta property="og:title" content="An Interactive Guide to Web Animation Performance • Siwalik Mukherjee"><meta property="og:type" content="website"><meta property="og:image" content="/opengraph-image.jpg"><meta property="og:url" content="https://www.siwalik.in/posts/interactive-guide-to-web-animation-performance/"><meta property="og:image:url" content="/opengraph-image.jpg"><meta property="og:image:alt" content="portfolio template image"><meta name="twitter:creator" content="@siwalikm"><link rel="icon" href="/avatar.png"><link rel="sitemap" href="/sitemap-index.xml"><meta content="width=device-width, initial-scale=1" name="viewport"><meta content="Astro v4.5.5" name="generator"><meta content="/opengraph-image.jpg" name="twitter:image"><meta content="summary_large_image" name="twitter:card"><meta content="An Interactive Guide to Web Animation Performance • Siwalik Mukherjee" name="twitter:title"><meta content="Siwalik Mukherjee - Software Engineer | Designer" name="twitter:description"><meta content="@siwalikm" name="twitter:site"><style>path[data-astro-cid-lqte4vf2].toc-marker{transition:stroke-dasharray .3s ease}
2</style>
3<link rel="stylesheet" href="/_astro/index.mj_hWzKT.css" />
4<style>.box-shadow-animate{height:200px;background:linear-gradient(130deg,#080210 11.2%,#261d31 78.9%);border-radius:8px;border:1px solid rgb(182 156 216);transition:all .3s ease-in-out}.box-shadow-animate:hover{box-shadow:0 0 30px #d6b5ff}.pseudo-element-box{height:200px;background:linear-gradient(141deg,#031726 11.2%,#123849 78.9%);border-radius:8px;border:1px solid rgb(178 246 255);position:relative}.pseudo-element-box:after{content:"";position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;border-radius:8px;box-shadow:0 0 30px #84c9ff;transition:opacity .3s ease-in-out}.pseudo-element-box:hover:after{opacity:1}.pipeline{height:60px;position:relative;background:#1c1c1c;border-radius:4px;overflow:hidden}.paint-bar{position:absolute;bottom:0;width:4px;background:#22c55e;animation:paintFlow 2s linear forwards}.paint-bar-sparse{position:absolute;bottom:0;width:4px;background:#22c55e;animation:paintFlowSparse 2s linear forwards}@keyframes paintFlow{0%{height:0px;transform:translate(0)}10%{height:45px}90%{height:45px}to{height:0px;transform:translate(40px)}}
4@keyframes paintFlowSparse{0%{height:0px;transform:translate(0)}10%{height:25px}90%{height:25px}to{height:0px;transform:translate(40px)}}
5</style>
5<script type="module" src="/_astro/hoisted.CnEIuPNn.js"></script>
vendor: 1 bytes, line 5
5
6<script type="module" src="/_astro/page.CY7HCSJH.js"></script>
vendor: 1 bytes, line 6
6
7<script>
8    (function () {
9      
10    var script = document.createElement("script");
11    var viewTransitionsEnabled = document.querySelector("meta[name='astro-view-transitions-enabled']")?.content;
12
13    script.setAttribute("src", "https://cloud.umami.is/script.js");
14    script.setAttribute("defer", true);
15    script.setAttribute("data-website-id", "c08c1602-3ba6-444f-a2a6-7bd90edbf477");
16    ;
17
18    if (!!viewTransitionsEnabled) {
19      script.setAttribute("data-astro-rerun", true);
20    }
21
22    var head = document.querySelector("head");
23    head.appendChild(script);
24  
25    })()
26  </script>
26<style>[data-astro-transition-scope="astro-zce5wc64-1"] { view-transition-name: blog-title-interactive-guide-to-web-animation-performance; }@layer astro { ::view-transition-old(blog-title-interactive-guide-to-web-animation-performance) { 
27	animation-duration: 180ms;
28	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
29	animation-fill-mode: both;
30	animation-name: astroFadeOut; }::view-transition-new(blog-title-interactive-guide-to-web-animation-performance) { 
31	animation-duration: 180ms;
32	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
33	animation-fill-mode: both;
34	animation-name: astroFadeIn; }[data-astro-transition=back]::view-transition-old(blog-title-interactive-guide-to-web-animation-performance) { 
35	animation-duration: 180ms;
36	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
37	animation-fill-mode: both;
38	animation-name: astroFadeOut; }[data-astro-transition=back]::view-transition-new(blog-title-interactive-guide-to-web-animation-performance) { 
39	animation-duration: 180ms;
40	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
41	animation-fill-mode: both;
42	animation-name: astroFadeIn; } }[data-astro-transition-fallback="old"] [data-astro-transition-scope="astro-zce5wc64-1"],
43			[data-astro-transition-fallback="old"][data-astro-transition-scope="astro-zce5wc64-1"] { 
44	animation-duration: 180ms;
45	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
46	animation-fill-mode: both;
47	animation-name: astroFadeOut; }[data-astro-transition-fallback="new"] [data-astro-transition-scope="astro-zce5wc64-1"],
48			[data-astro-transition-fallback="new"][data-astro-transition-scope="astro-zce5wc64-1"] { 
49	animation-duration: 180ms;
50	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
51	animation-fill-mode: both;
52	animation-name: astroFadeIn; }[data-astro-transition=back][data-astro-transition-fallback="old"] [data-astro-transition-scope="astro-zce5wc64-1"],
53			[data-astro-transition=back][data-astro-transition-fallback="old"][data-astro-transition-scope="astro-zce5wc64-1"] { 
54	animation-duration: 180ms;
55	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
56	animation-fill-mode: both;
57	animation-name: astroFadeOut; }[data-astro-transition=back][data-astro-transition-fallback="new"] [data-astro-transition-scope="astro-zce5wc64-1"],
58			[data-astro-transition=back][data-astro-transition-fallback="new"][data-astro-transition-scope="astro-zce5wc64-1"] { 
59	animation-duration: 180ms;
60	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
61	animation-fill-mode: both;
62	animation-name: astroFadeIn; }</style><style>[data-astro-transition-scope="astro-zlpcrqvn-2"] { view-transition-name: blog-desc-interactive-guide-to-web-animation-performance; }@layer astro { ::view-transition-old(blog-desc-interactive-guide-to-web-animation-performance) { 
63	animation-duration: 180ms;
64	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
65	animation-fill-mode: both;
66	animation-name: astroFadeOut; }::view-transition-new(blog-desc-interactive-guide-to-web-animation-performance) { 
67	animation-duration: 180ms;
68	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
69	animation-fill-mode: both;
70	animation-name: astroFadeIn; }[data-astro-transition=back]::view-transition-old(blog-desc-interactive-guide-to-web-animation-performance) { 
71	animation-duration: 180ms;
72	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
73	animation-fill-mode: both;
74	animation-name: astroFadeOut; }[data-astro-transition=back]::view-transition-new(blog-desc-interactive-guide-to-web-animation-performance) { 
75	animation-duration: 180ms;
76	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
77	animation-fill-mode: both;
78	animation-name: astroFadeIn; } }[data-astro-transition-fallback="old"] [data-astro-transition-scope="astro-zlpcrqvn-2"],
79			[data-astro-transition-fallback="old"][data-astro-transition-scope="astro-zlpcrqvn-2"] { 
80	animation-duration: 180ms;
81	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
82	animation-fill-mode: both;
83	animation-name: astroFadeOut; }[data-astro-transition-fallback="new"] [data-astro-transition-scope="astro-zlpcrqvn-2"],
84			[data-astro-transition-fallback="new"][data-astro-transition-scope="astro-zlpcrqvn-2"] { 
85	animation-duration: 180ms;
86	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
87	animation-fill-mode: both;
88	animation-name: astroFadeIn; }[data-astro-transition=back][data-astro-transition-fallback="old"] [data-astro-transition-scope="astro-zlpcrqvn-2"],
89			[data-astro-transition=back][data-astro-transition-fallback="old"][data-astro-transition-scope="astro-zlpcrqvn-2"] { 
90	animation-duration: 180ms;
91	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
92	animation-fill-mode: both;
93	animation-name: astroFadeOut; }[data-astro-transition=back][data-astro-transition-fallback="new"] [data-astro-transition-scope="astro-zlpcrqvn-2"],
94			[data-astro-transition=back][data-astro-transition-fallback="new"][data-astro-transition-scope="astro-zlpcrqvn-2"] { 
95	animation-duration: 180ms;
96	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
97	animation-fill-mode: both;
98	animation-name: astroFadeIn; }</style><style>[data-astro-transition-scope="astro-3ukziwy4-3"] { view-transition-name: blog-min-interactive-guide-to-web-animation-performance; }@layer astro { ::view-transition-old(blog-min-interactive-guide-to-web-animation-performance) { 
99	animation-duration: 180ms;
100	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
101	animation-fill-mode: both;
102	animation-name: astroFadeOut; }::view-transition-new(blog-min-interactive-guide-to-web-animation-performance) { 
103	animation-duration: 180ms;
104	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
105	animation-fill-mode: both;
106	animation-name: astroFadeIn; }[data-astro-transition=back]::view-transition-old(blog-min-interactive-guide-to-web-animation-performance) { 
107	animation-duration: 180ms;
108	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
109	animation-fill-mode: both;
110	animation-name: astroFadeOut; }[data-astro-transition=back]::view-transition-new(blog-min-interactive-guide-to-web-animation-performance) { 
111	animation-duration: 180ms;
112	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
113	animation-fill-mode: both;
114	animation-name: astroFadeIn; } }[data-astro-transition-fallback="old"] [data-astro-transition-scope="astro-3ukziwy4-3"],
115			[data-astro-transition-fallback="old"][data-astro-transition-scope="astro-3ukziwy4-3"] { 
116	animation-duration: 180ms;
117	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
118	animation-fill-mode: both;
119	animation-name: astroFadeOut; }[data-astro-transition-fallback="new"] [data-astro-transition-scope="astro-3ukziwy4-3"],
120			[data-astro-transition-fallback="new"][data-astro-transition-scope="astro-3ukziwy4-3"] { 
121	animation-duration: 180ms;
122	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
123	animation-fill-mode: both;
124	animation-name: astroFadeIn; }[data-astro-transition=back][data-astro-transition-fallback="old"] [data-astro-transition-scope="astro-3ukziwy4-3"],
125			[data-astro-transition=back][data-astro-transition-fallback="old"][data-astro-transition-scope="astro-3ukziwy4-3"] { 
126	animation-duration: 180ms;
127	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
128	animation-fill-mode: both;
129	animation-name: astroFadeOut; }[data-astro-transition=back][data-astro-transition-fallback="new"] [data-astro-transition-scope="astro-3ukziwy4-3"],
130			[data-astro-transition=back][data-astro-transition-fallback="new"][data-astro-transition-scope="astro-3ukziwy4-3"] { 
131	animation-duration: 180ms;
132	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
133	animation-fill-mode: both;
134	animation-name: astroFadeIn; }</style><style>[data-astro-transition-scope="astro-seagkckf-4"] { view-transition-name: blog-date-interactive-guide-to-web-animation-performance; }@layer astro { ::view-transition-old(blog-date-interactive-guide-to-web-animation-performance) { 
135	animation-duration: 180ms;
136	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
137	animation-fill-mode: both;
138	animation-name: astroFadeOut; }::view-transition-new(blog-date-interactive-guide-to-web-animation-performance) { 
139	animation-duration: 180ms;
140	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
141	animation-fill-mode: both;
142	animation-name: astroFadeIn; }[data-astro-transition=back]::view-transition-old(blog-date-interactive-guide-to-web-animation-performance) { 
143	animation-duration: 180ms;
144	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
145	animation-fill-mode: both;
146	animation-name: astroFadeOut; }[data-astro-transition=back]::view-transition-new(blog-date-interactive-guide-to-web-animation-performance) { 
147	animation-duration: 180ms;
148	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
149	animation-fill-mode: both;
150	animation-name: astroFadeIn; } }[data-astro-transition-fallback="old"] [data-astro-transition-scope="astro-seagkckf-4"],
151			[data-astro-transition-fallback="old"][data-astro-transition-scope="astro-seagkckf-4"] { 
152	animation-duration: 180ms;
153	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
154	animation-fill-mode: both;
155	animation-name: astroFadeOut; }[data-astro-transition-fallback="new"] [data-astro-transition-scope="astro-seagkckf-4"],
156			[data-astro-transition-fallback="new"][data-astro-transition-scope="astro-seagkckf-4"] { 
157	animation-duration: 180ms;
158	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
159	animation-fill-mode: both;
160	animation-name: astroFadeIn; }[data-astro-transition=back][data-astro-transition-fallback="old"] [data-astro-transition-scope="astro-seagkckf-4"],
161			[data-astro-transition=back][data-astro-transition-fallback="old"][data-astro-transition-scope="astro-seagkckf-4"] { 
162	animation-duration: 180ms;
163	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
164	animation-fill-mode: both;
165	animation-name: astroFadeOut; }[data-astro-transition=back][data-astro-transition-fallback="new"] [data-astro-transition-scope="astro-seagkckf-4"],
166			[data-astro-transition=back][data-astro-transition-fallback="new"][data-astro-transition-scope="astro-seagkckf-4"] { 
167	animation-duration: 180ms;
168	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
169	animation-fill-mode: both;
170	animation-name: astroFadeIn; }</style><style>[data-astro-transition-scope="astro-zrejx7iv-5"] { view-transition-name: blog-cover-interactive-guide-to-web-animation-performance; }@layer astro { ::view-transition-old(blog-cover-interactive-guide-to-web-animation-performance) { animation: none; opacity: 0; mix-blend-mode: normal; }::view-transition-new(blog-cover-interactive-guide-to-web-animation-performance) { animation: none; mix-blend-mode: normal; }::view-transition-group(blog-cover-interactive-guide-to-web-animation-performance) { animation: none } }[data-astro-transition-fallback="old"] [data-astro-transition-scope="astro-zrejx7iv-5"],
171			[data-astro-transition-fallback="old"][data-astro-transition-scope="astro-zrejx7iv-5"] { animation: none; mix-blend-mode: normal; }[data-astro-transition-fallback="new"] [data-astro-transition-scope="astro-zrejx7iv-5"],
172			[data-astro-transition-fallback="new"][data-astro-transition-scope="astro-zrejx7iv-5"] { animation: none; mix-blend-mode: normal; }</style><style>[data-astro-transition-scope="astro-cbmzjlw4-6"] { view-transition-name: avatar-image; }@layer astro { ::view-transition-old(avatar-image) { 
173	animation-duration: 180ms;
174	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
175	animation-fill-mode: both;
176	animation-name: astroFadeOut; }::view-transition-new(avatar-image) { 
177	animation-duration: 180ms;
178	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
179	animation-fill-mode: both;
180	animation-name: astroFadeIn; }[data-astro-transition=back]::view-transition-old(avatar-image) { 
181	animation-duration: 180ms;
182	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
183	animation-fill-mode: both;
184	animation-name: astroFadeOut; }[data-astro-transition=back]::view-transition-new(avatar-image) { 
185	animation-duration: 180ms;
186	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
187	animation-fill-mode: both;
188	animation-name: astroFadeIn; } }[data-astro-transition-fallback="old"] [data-astro-transition-scope="astro-cbmzjlw4-6"],
189			[data-astro-transition-fallback="old"][data-astro-transition-scope="astro-cbmzjlw4-6"] { 
190	animation-duration: 180ms;
191	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
192	animation-fill-mode: both;
193	animation-name: astroFadeOut; }[data-astro-transition-fallback="new"] [data-astro-transition-scope="astro-cbmzjlw4-6"],
194			[data-astro-transition-fallback="new"][data-astro-transition-scope="astro-cbmzjlw4-6"] { 
195	animation-duration: 180ms;
196	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
197	animation-fill-mode: both;
198	animation-name: astroFadeIn; }[data-astro-transition=back][data-astro-transition-fallback="old"] [data-astro-transition-scope="astro-cbmzjlw4-6"],
199			[data-astro-transition=back][data-astro-transition-fallback="old"][data-astro-transition-scope="astro-cbmzjlw4-6"] { 
200	animation-duration: 180ms;
201	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
202	animation-fill-mode: both;
203	animation-name: astroFadeOut; }[data-astro-transition=back][data-astro-transition-fallback="new"] [data-astro-transition-scope="astro-cbmzjlw4-6"],
204			[data-astro-transition=back][data-astro-transition-fallback="new"][data-astro-transition-scope="astro-cbmzjlw4-6"] { 
205	animation-duration: 180ms;
206	animation-timing-function: cubic-bezier(0.76, 0, 0.24, 1);
207	animation-fill-mode: both;
208	animation-name: astroFadeIn; }</style></head> <body class="mx-auto mt-12 flex h-full min-h-screen w-full max-w-screen-lg flex-col gap-8 bg-color-background pt-12 text-color-font-primary"> <header class="fixed left-0 top-0 z-40 w-full bg-color-background/100 backdrop-blur-sm sm:bg-color-background/60" data-astro-cid-5blmo7yk> <div class="px-4 lg:mx-auto lg:max-w-screen-lg" data-astro-cid-5blmo7yk> <nav class="flex h-full flex-wrap items-center justify-between py-4 sm:h-20" role="navigation" data-astro-cid-5blmo7yk> <div class="flex-no-shrink mr-6 flex items-center text-white" data-astro-cid-5blmo7yk> <a href="/" class="group block no-underline hover:text-white sm:mt-0 sm:inline-block" data-astro-cid-5blmo7yk> <div class="group relative flex cursor-pointer items-center gap-4 font-semibold" data-astro-cid-5blmo7yk>  <img src="/_astro/avatar.WAYowfe4_Z1GNtUA.webp" alt="A description of my image." class="z-10 rounded-full bg-white transition-opacity duration-300" data-astro-cid-5blmo7yk data-astro-transition-scope="astro-cbmzjlw4-6" width="48" height="48" loading="lazy" decoding="async"> <span class="absolute inset-0 z-0 -mr-14 flex translate-x-6 items-center rounded-xl  px-2 opacity-0 transition-all duration-300 group-hover:-translate-x-12 group-hover:opacity-100" data-astro-cid-5blmo7yk> <svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chevron-left" data-astro-cid-5blmo7yk="true"><path d="m15 18-6-6 6-6"></path></svg> </span>  <div class="flex items-center gap-1" data-astro-cid-5blmo7yk> <span class="items-center text-base transition-all duration-300 ease-out group-focus:text-color-accent" data-astro-cid-5blmo7yk>
208Siwalik Mukherjee</span> <svg fill="#1da1f2" class="icon icon--verified" height="24" role="img" width="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" data-astro-cid-5blmo7yk><path d="M22.5 12.5c0-1.58-.875-2.95-2.148-3.6.154-.435.238-.905.238-1.4 0-2.21-1.71-3.998-3.818-3.998-.47 0-.92.084-1.336.25C14.818 2.415 13.51 1.5 12 1.5s-2.816.917-3.437 2.25c-.415-.165-.866-.25-1.336-.25-2.11 0-3.818 1.79-3.818 4 0 .494.083.964.237 1.4-1.272.65-2.147 2.018-2.147 3.6 0 1.495.782 2.798 1.942 3.486-.02.17-.032.34-.032.514 0 2.21 1.708 4 3.818 4 .47 0 .92-.086 1.335-.25.62 1.334 1.926 2.25 3.437 2.25 1.512 0 2.818-.916 3.437-2.25.415.163.865.248 1.336.248 2.11 0 3.818-1.79 3.818-4 0-.174-.012-.344-.033-.513 1.158-.687 1.943-1.99 1.943-3.484zm-6.616-3.334l-4.334 6.5c-.145.217-.382.334-.625.334-.143 0-.288-.04-.416-.126l-.115-.094-2.415-2.415c-.293-.293-.293-.768 0-1.06s.768-.294 1.06 0l1.77 1.767 3.825-5.74c.23-.345.696-.436 1.04-.207.346.23.44.696.21 1.04z" data-astro-cid-5blmo7yk></path></svg> </div> </div> </a> </div> <div class="block sm:hidden" data-astro-cid-5blmo7yk> <button class="mobile-nav-toggle flex h-12 w-12 items-center justify-center rounded-full bg-color-background-light hover:border-white hover:text-white" data-astro-cid-5blmo7yk> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-menu" data-astro-cid-5blmo7yk="true"><line x1="4" x2="20" y1="12" y2="12"></line><line x1="4" x2="20" y1="6" y2="6"></line><line x1="4" x2="20" y1="18" y2="18"></line></svg> </button> </div> <div class="nav-menu-items hidden h-screen w-full flex-grow sm:flex sm:h-auto sm:w-auto sm:items-center" data-astro-cid-5blmo7yk> <div class="text-sm sm:flex-grow" data-astro-cid-5blmo7yk></div> <div class="flex h-screen flex-col items-center justify-center text-2xl font-semibold sm:block sm:h-auto sm:text-base [&>*]:ml-4" data-astro-cid-5blmo7yk> <span class="block sm:hidden mb-1" data-astro-cid-5blmo7yk> <a class="group inline-block" href="/" target="_self">home<div class="bg-color-accent h-[4px] mt-1 w-0 group-hover:w-full group-focus:w-full transition-all duration-300 rounded-xl w-full"></div></a> </span><span class="mb-1" data-astro-cid-5blmo7yk> <a class="group inline-block" href="/posts" target="_self">posts<div class="bg-color-accent h-[4px] mt-1 w-0 group-hover:w-full group-focus:w-full transition-all duration-300 rounded-xl w-full"></div></a> </span><span class="mb-1" data-astro-cid-5blmo7yk> <a class="group inline-block" href="/projects" target="_self">projects<div class="bg-color-accent h-[4px] mt-1 w-0 group-hover:w-full group-focus:w-full transition-all duration-300 rounded-xl"></div></a> </span><span class="mb-1" data-astro-cid-5blmo7yk> <a class="group inline-block" href="/uses" target="_self">uses<div class="bg-color-accent h-[4px] mt-1 w-0 group-hover:w-full group-focus:w-full transition-all duration-300 rounded-xl"></div></a> </span> </div> </div> </nav> </div> <div class="scroll-progress bg-slate-700" data-astro-cid-5blmo7yk></div> </header>  
208<script>(function(){const NAV_ITEMS = [{"label":"home","href":"/"},{"label":"posts","href":"/posts"},{"label":"projects","href":"/projects"},{"label":"uses","href":"/uses"}];
209
210  document.addEventListener('astro:page-load', () => {
211    document.querySelector('.mobile-nav-toggle').addEventListener('click', () => {
212      document.querySelector('.nav-menu-items').classList.toggle('hidden');
213    });
214  });
215})();</script>
215  <header role="presentation" class="mt-6 flex max-w-[1000px] rounded-xl px-4"> <div> <h1 class="py-1 text-4xl md:text-6xl" data-astro-transition-scope="astro-zce5wc64-1"> <strong> An Interactive Guide to Web Animation Performance </strong> </h1> <p class="description my-2 text-color-font-tertiary" data-astro-transition-scope="astro-zlpcrqvn-2"> Why does your animation jank on Android but not your MacBook? An interactive guide to how browsers render frames, and the techniques to fix it — FLIP, GPU layers, scroll-driven CSS, and more. </p> <div class="flex"> <p class="flex items-center gap-2 text-sm" data-astro-transition-scope="astro-3ukziwy4-3"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-book-open-text"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path><path d="M6 8h2"></path><path d="M6 12h2"></path><path d="M16 8h2"></path><path d="M16 12h2"></path></svg> 10 min read </p> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-dot"><circle cx="12.1" cy="12.1" r="1"></circle></svg> <p class="flex items-center gap-2 text-sm" data-astro-transition-scope="astro-seagkckf-4"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar"><path d="M8 2v4"></path><path d="M16 2v4"></path><rect width="18" height="18" x="3" y="4" rx="2"></rect><path d="M3 10h18"></path></svg> Mar 29, 2026 </p> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-dot"><circle cx="12.1" cy="12.1" r="1"></circle></svg> <p class="flex items-center gap-2 text-sm"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-library"><path d="m16 6 4 14"></path><path d="M12 6v14"></path><path d="M8 8v12"></path><path d="M4 4v16"></path></svg> technical </p> </div> </div> </header> <!-- Bars trigger --><button id="toc-mini-trigger" aria-label="Table of contents" class="fixed right-3 top-1/2 z-50 -mr-2 flex -translate-y-1/2 cursor-pointer flex-col gap-[5px] p-2 md:hidden"> <span data-slug="flip-book" class="toc-mini-bar block h-[3px] w-5 rounded-full opacity-40 transition-all duration-300"></span><span data-slug="the-pixel-rendering-pipeline" class="toc-mini-bar block h-[3px] w-5 rounded-full opacity-40 transition-all duration-300"></span><span data-slug="shadow-boxing" class="toc-mini-bar block h-[3px] w-5 rounded-full opacity-40 transition-all duration-300"></span><span data-slug="layer-cake" class="toc-mini-bar block h-[3px] w-5 rounded-full opacity-40 transition-all duration-300"></span><span data-slug="flip-it" class="toc-mini-bar block h-[3px] w-5 rounded-full opacity-40 transition-all duration-300"></span><span data-slug="scroll-driven-animations" class="toc-mini-bar block h-[3px] w-5 rounded-full opacity-40 transition-all duration-300"></span><span data-slug="120hz-problems" class="toc-mini-bar block h-[3px] w-5 rounded-full opacity-40 transition-all duration-300"></span><span data-slug="prefers-reduced-motion" class="toc-mini-bar block h-[3px] w-5 rounded-full opacity-40 transition-all duration-300"></span><span data-slug="jank-hunting" class="toc-mini-bar block h-[3px] w-5 rounded-full opacity-40 transition-all duration-300"></span><span data-slug="closing-the-loop" class="toc-mini-bar block h-[3px] w-5 rounded-full opacity-40 transition-all duration-300"></span><span data-slug="relevant-reads" class="toc-mini-bar block h-[3px] w-5 rounded-full opacity-40 transition-all duration-300"></span> </button> <!-- Floating dropdown — content injected via slot --> <div id="toc-mini-dropdown" class="bg-color-bg-secondary fixed right-12 top-1/2 z-40 hidden w-56 -translate-y-1/2 roun
215ded-xl border border-white/10 bg-color-background-light p-4 shadow-xl md:hidden">  <nav class="toc sticky pt-2 text-sm" data-astro-cid-lqte4vf2> <ul class="my-1 grid pl-2" data-astro-cid-lqte4vf2> <li class="toc-wrapper grid"> <a href="#flip-book" class="mb-[4px] py-1">Flip Book</a>  </li><li class="toc-wrapper grid"> <a href="#the-pixel-rendering-pipeline" class="mb-[4px] py-1">The Pixel Rendering Pipeline</a>  </li><li class="toc-wrapper grid"> <a href="#shadow-boxing" class="mb-[4px] py-1">Shadow Boxing</a>  </li><li class="toc-wrapper grid"> <a href="#layer-cake" class="mb-[4px] py-1">Layer Cake</a>  </li><li class="toc-wrapper grid"> <a href="#flip-it" class="mb-[4px] py-1">FLIP It</a>  </li><li class="toc-wrapper grid"> <a href="#scroll-driven-animations" class="mb-[4px] py-1">Scroll-Driven Animations</a>  </li><li class="toc-wrapper grid"> <a href="#120hz-problems" class="mb-[4px] py-1">120Hz Problems</a>  </li><li class="toc-wrapper grid"> <a href="#prefers-reduced-motion" class="mb-[4px] py-1">prefers-reduced-motion</a>  </li><li class="toc-wrapper grid"> <a href="#jank-hunting" class="mb-[4px] py-1">Jank Hunting</a>  </li><li class="toc-wrapper grid"> <a href="#closing-the-loop" class="mb-[4px] py-1">Closing the Loop</a>  </li><li class="toc-wrapper grid"> <a href="#relevant-reads" class="mb-[4px] py-1">Relevant Reads</a>  </li> </ul> <svg class="toc-progress pointer-events-none absolute -left-2 top-0 h-full w-full text-color-accent" xmlns="http://www.w3.org/2000/svg" data-astro-cid-lqte4vf2> <path class="toc-marker" fill="none" stroke="currentColor" stroke-width="4" stroke-dasharray="1 0 1000 1000" stroke-dashoffset="1" stroke-linecap="round" stroke-linejoin="round" data-astro-cid-lqte4vf2></path> </svg> </nav>    </div>  <main class="flex items-start gap-12"> <div class="grid max-w-lg justify-items-start gap-8 px-4 md:max-w-2xl"> <article class="post-body prose-lg prose-invert max-w-prose text-lg"> <!-- <div class="not-prose relative xl:w-[calc(100%+110px)] xl:-left-[55px] left-0 w-auto"> --> <div class="not-prose"> <img src="/_astro/web-animation-performance-pixel-rendering-pipeline.CiTnMRDc_Z1BFDb9.webp" srcset="/_astro/web-animation-performance-pixel-rendering-pipeline.CiTnMRDc_kVUy5.webp 240w, /_astro/web-animation-performance-pixel-rendering-pipeline.CiTnMRDc_Z1zW6OP.webp 540w, /_astro/web-animation-performance-pixel-rendering-pipeline.CiTnMRDc_r6GyO.webp 720w, /_astro/web-animation-performance-pixel-rendering-pipeline.CiTnMRDc_Z2oBQco.webp 960w" sizes="(max-width: 360px) 240px, (max-width: 720px) 540px, (max-width: 1600px) 720px, 960px" alt="An Interactive Guide to Web Animation Performance" data-astro-transition-scope="astro-zrejx7iv-5" width="2688" height="1536" loading="lazy" decoding="async"> </div> <p>Animation performance advice can start to feel like superstition: use <code>transform</code> not <code>width</code>, never animate <code>height</code>, avoid layout triggers. The rules are real – but without knowing <em>why</em>, you’re just hoping they cover your situation. Eventually, they won’t.</p>
216<p>Tools like (Framer) Motion and GSAP help a lot. But they can’t fix a misunderstood rendering pipeline. This is an interactive guide to how browsers actually handle animations, where the real costs hide, and how to outmaneuver them. Let’s get into it!</p>
217<div class="block sm:hidden"><div class="relative mx-2 my-8"><div style="outline:8px solid #18181b" class="
218          absolute 
219          -left-4
220          -top-4
221          h-10 
222          w-10 
223          rounded-full 
224          bg-color-background
225          border-amber-500
226          z-10 
227          flex 
228          items-center 
229          justify-center
230          border-2
231          shadow-lg
232        "><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-notebook-pen stroke-current text-amber-500"><path d="M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4"></path><path d="M2 6h4"></path><path d="M2 10h4"></path><path d="M2 14h4"></path><path d="M2 18h4"></path><path d="M21.378 5.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"></path></svg></div><div class="
233          relative
234          rounded-lg
235          border-2 
236          bg-zinc-900
237          border-amber-500
238          p-4
239          pt-2
240        "><div class="mb-3 flex items-center gap-2"><div class="text-md absolute -top-4 right-4 bg-color-background px-2 text-amber-500">note</div></div><div class="text-zinc-300 child-style"><p>This post is heavy on animations and interactive demos. For the best experience, view this post on a wider screen like tablet or desktop.</p></div><style>
241                .child-style * {
242                    margin: 0;
243                }
244                .child-style *:not(:last-child) {
245                   margin-bottom: 16px; /* Adjust the margin value as needed */
246                }
247            </style></div></div></div>
248<section class="heading" data-heading-rank="2"><h2 id="flip-book">Flip Book</h2>
249<p>Every silky smooth motion is just a rapid sequence of still images, like a digital flipbook. The faster you flip, the smoother it looks.</p>
250<style>astro-island,astro-slot,astro-static-slot{display:contents}</style>
250<script>(()=>{var e=async t=>{await(await t())()};(self.Astro||(self.Astro={})).only=e;window.dispatchEvent(new Event("astro:only"));})();;(()=>{var v=Object.defineProperty;var A=(c,s,a)=>s in c?v(c,s,{enumerable:!0,configurable:!0,writable:!0,value:a}):c[s]=a;var d=(c,s,a)=>(A(c,typeof s!="symbol"?s+"":s,a),a);var u;{let c={0:t=>m(t),1:t=>a(t),2:t=>new RegExp(t),3:t=>new Date(t),4:t=>new Map(a(t)),5:t=>new Set(a(t)),6:t=>BigInt(t),7:t=>new URL(t),8:t=>new Uint8Array(t),9:t=>new Uint16Array(t),10:t=>new Uint32Array(t)},s=t=>{let[e,n]=t;return e in c?c[e](n):void 0},a=t=>t.map(s),m=t=>typeof t!="object"||t===null?t:Object.fromEntries(Object.entries(t).map(([e,n])=>[e,s(n)]));customElements.get("astro-island")||customElements.define("astro-island",(u=class extends HTMLElement{constructor(){super(...arguments);d(this,"Component");d(this,"hydrator");d(this,"hydrate",async()=>{var f;if(!this.hydrator||!this.isConnected)return;let e=(f=this.parentElement)==null?void 0:f.closest("astro-island[ssr]");if(e){e.addEventListener("astro:hydrate",this.hydrate,{once:!0});return}let n=this.querySelectorAll("astro-slot"),r={},l=this.querySelectorAll("template[data-astro-template]");for(let o of l){let i=o.closest(this.tagName);i!=null&&i.isSameNode(this)&&(r[o.getAttribute("data-astro-template")||"default"]=o.innerHTML,o.remove())}for(let o of n){let i=o.closest(this.tagName);i!=null&&i.isSameNode(this)&&(r[o.getAttribute("name")||"default"]=o.innerHTML)}let h;try{h=this.hasAttribute("props")?m(JSON.parse(this.getAttribute("props"))):{}}catch(o){let i=this.getAttribute("component-url")||"<unknown>",b=this.getAttribute("component-export");throw b&&(i+=` (export ${b})`),console.error(`[hydrate] Error parsing props for component ${i}`,this.getAttribute("props"),o),o}let p;await this.hydrator(this)(this.Component,h,r,{client:this.getAttribute("client")}),this.removeAttribute("ssr"),this.dispatchEvent(new CustomEvent("astro:hydrate"))});d(this,"unmount",()=>{this.isConnected||this.dispatchEvent(new CustomEvent("astro:unmount"))})}disconnectedCallback(){document.removeEventListener("astro:after-swap",this.unmount),document.addEventListener("astro:after-swap",this.unmount,{once:!0})}connectedCallback(){if(!this.hasAttribute("await-children")||document.readyState==="interactive"||document.readyState==="complete")this.childrenConnectedCallback();else{let e=()=>{document.removeEventListener("DOMContentLoaded",e),n.disconnect(),this.childrenConnectedCallback()},n=new MutationObserver(()=>{var r;((r=this.lastChild)==null?void 0:r.nodeType)===Node.COMMENT_NODE&&this.lastChild.nodeValue==="astro:end"&&(this.lastChild.remove(),e())});n.observe(this,{childList:!0}),document.addEventListener("DOMContentLoaded",e)}}async childrenConnectedCallback(){let e=this.getAttribute("before-hydration-url");e&&await import(e),this.start()}async start(){let e=JSON.parse(this.getAttribute("opts")),n=this.getAttribute("client");if(Astro[n]===void 0){window.addEventListener(`astro:${n}`,()=>this.start(),{once:!0});return}try{await Astro[n](async()=>{let r=this.getAttribute("renderer-url"),[l,{default:h}]=await Promise.all([import(this.getAttribute("component-url")),r?import(r):()=>()=>{}]),p=this.getAttribute("component-export")||"default";if(!p.includes("."))this.Component=l[p];else{this.Component=l;for(let y of p.split("."))this.Component=this.Component[y]}return this.hydrator=h,this.hydrate},e,this)}catch(r){console.error(`[astro-island] Error hydrating ${this.getAttribute("component-url")}`,r)}}attributeChangedCallback(){this.hydrate()}},d(u,"observedAttributes",["props"]),u))}})();</script>
250<astro-island uid="103moU" component-url="/_astro/FlipbookPlayer.CGShrecD.js" component-export="default" renderer-url="/_astro/client.OBjSpQF5.js" props="{}" ssr="" client="only" opts="{&quot;name&quot;:&quot;FlipbookPlayer&quot;,&quot;value&quot;:&quot;react&quot;}"></astro-island>
251<p>On screen, each of those images is a <strong>frame</strong> drawn by the browser. At 60 FPS, you have <strong>16.7 ms</strong> to render each one, in 120 FPS, you have <strong>8.3 ms</strong>. Miss that budget and you drop a frame. Drop enough frames and the animation feels janky.</p>
252<astro-island uid="ZmNaRC" component-url="/_astro/FPSTimelineDemo.DNPJtCb5.js" component-export="default" renderer-url="/_astro/client.OBjSpQF5.js" props="{}" ssr="" client="only" opts="{&quot;name&quot;:&quot;FPSTimelineDemo&quot;,&quot;value&quot;:&quot;react&quot;}"></astro-island>
253</section><section class="heading" data-heading-rank="2"><h2 id="the-pixel-rendering-pipeline">The Pixel Rendering Pipeline</h2>
254<p>Every frame goes through a series of stages inside the browser. Triggering an earlier stage cascades into everything that follows, so the stage you hit determines the cost you pay.</p>
255<astro-island uid="ZQNsIh" component-url="/_astro/RenderPipelineDemo.CU4dCMeA.js" component-export="default" renderer-url="/_astro/client.OBjSpQF5.js" props="{}" ssr="" client="only" opts="{&quot;name&quot;:&quot;RenderPipelineDemo&quot;,&quot;value&quot;:&quot;react&quot;}"></astro-island>
256<div class="relative mx-2 my-8"><div style="outline:8px solid #18181b" class="
257          absolute 
258          -left-4
259          -top-4
260          h-10 
261          w-10 
262          rounded-full 
263          bg-color-background
264          border-amber-500
265          z-10 
266          flex 
267          items-center 
268          justify-center
269          border-2
270          shadow-lg
271        "><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-notebook-pen stroke-current text-amber-500"><path d="M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4"></path><path d="M2 6h4"></path><path d="M2 10h4"></path><path d="M2 14h4"></path><path d="M2 18h4"></path><path d="M21.378 5.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"></path></svg></div><div class="
272          relative
273          rounded-lg
274          border-2 
275          bg-zinc-900
276          border-amber-500
277          p-4
278          pt-2
279        "><div class="mb-3 flex items-center gap-2"><div class="text-md absolute -top-4 right-4 bg-color-background px-2 text-amber-500">note</div></div><div class="text-zinc-300 child-style"><p>Worth burning into your brain: if you trigger Layout, you also pay for Paint and Composite. But if you only trigger Composite - via <code>transform</code> or <code>opacity</code>, you skip the expensive stuff entirely. This one rule explains most of the advice in this post.</p></div><style>
280                .child-style * {
281                    margin: 0;
282                }
283                .child-style *:not(:last-child) {
284                   margin-bottom: 16px; /* Adjust the margin value as needed */
285                }
286            </style></div></div>
287</section><section class="heading" data-heading-rank="2"><h2 id="shadow-boxing">Shadow Boxing</h2>
288<p>Some animations look simple but are quietly expensive. Can we use the pipeline to outsmart them?</p>
289<p>Consider box-shadow animations. Each frame of a <code>box-shadow</code> transition triggers <strong>Paint → Composite</strong>. Shadow calculations and Gaussian blur get re-executed on every single frame.</p>
290<div id="shadow-normal"><div class="expressive-code"><link rel="stylesheet" href="/_astro/ec.ngsrv.css"/>
290<script type="module" src="/_astro/ec.dy9ns.js"></script>
290<figure class="frame"><figcaption class="header"></figcaption><pre tabindex="0" class="wrap" style="--ecMaxLine:44ch"><code><div class="ec-line"><div class="gutter"><div class="ln">1</div></div><div class="code"><span style="--0:#9FA3AA;--0fs:italic">/* Usual solution */</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">2</div></div><div class="code"><span style="--0:#9FA3AA;--0fs:italic">/* triggers Paint → Composite every frame */</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">3</div></div><div class="code">
291</div></div><div class="ec-line"><div class="gutter"><div class="ln">4</div></div><div class="code"><span style="--0:#D19A66">.card</span><span style="--0:#ABB2BF"> {</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">5</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">  </span></span><span style="--0:#ABB2BF">transition: box-shadow </span><span style="--0:#D19A66">0.3</span><span style="--0:#E6888F">s</span><span style="--0:#ABB2BF"> </span><span style="--0:#D19A66">ease</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">6</div></div><div class="code"><span style="--0:#ABB2BF">}</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">7</div></div><div class="code">
292</div></div><div class="ec-line"><div class="gutter"><div class="ln">8</div></div><div class="code"><span style="--0:#D19A66">.card</span><span style="--0:#56B6C2">:hover</span><span style="--0:#ABB2BF"> {</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">9</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">  </span></span><span style="--0:#ABB2BF">box-shadow: </span><span style="--0:#D19A66">0</span><span style="--0:#ABB2BF"> </span><span style="--0:#D19A66">10</span><span style="--0:#E6888F">px</span><span style="--0:#ABB2BF"> </span><span style="--0:#D19A66">20</span><span style="--0:#E6888F">px</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">rgba</span><span style="--0:#ABB2BF">(</span><span style="--0:#D19A66">0</span><span style="--0:#ABB2BF">,</span><span style="--0:#D19A66">0</span><span style="--0:#ABB2BF">,</span><span style="--0:#D19A66">0</span><span style="--0:#ABB2BF">,</span><span style="--0:#D19A66">0.2</span><span style="--0:#ABB2BF">);</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">10</div></div><div class="code"><span style="--0:#ABB2BF">}</span></div></div></code></pre><div class="copy"><button title="Copy to clipboard" data-copied="Copied!" data-code="/* Usual solution */�/* triggers Paint → Composite every frame */��.card {�  transition: box-shadow 0.3s ease;�}��.card:hover {�  box-shadow: 0 10px 20px rgba(0,0,0,0.2);�}"><div></div></button></div></figure></div></div>
293<p>Here’s a more efficient approach: pre-render the shadow on a pseudo-element and animate its <code>opacity</code> from 0 to 1. This skips the Paint stage entirely and goes straight to Composite.</p>
294<div id="shadow-boosted"><div class="expressive-code"><figure class="frame"><figcaption class="header"></figcaption><pre tabindex="0" class="wrap" style="--ecMaxLine:47ch"><code><div class="ec-line"><div class="gutter"><div class="ln">1</div></div><div class="code"><span style="--0:#9FA3AA;--0fs:italic">/* GPU-accelerated solution */</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">2</div></div><div class="code"><span style="--0:#9FA3AA;--0fs:italic">/* triggers only Composite */</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">3</div></div><div class="code">
295</div></div><div class="ec-line"><div class="gutter"><div class="ln">4</div></div><div class="code"><span style="--0:#D19A66">.card</span><span style="--0:#ABB2BF"> {</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">5</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">  </span></span><span style="--0:#ABB2BF">position: </span><span style="--0:#D19A66">relative</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">6</div></div><div class="code"><span style="--0:#ABB2BF">}</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">7</div></div><div class="code">
296</div></div><div class="ec-line"><div class="gutter"><div class="ln">8</div></div><div class="code"><span style="--0:#D19A66">.card</span><span style="--0:#56B6C2">::after</span><span style="--0:#ABB2BF"> {</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">9</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">  </span></span><span style="--0:#ABB2BF">content: </span><span style="--0:#98C379">&#39;&#39;</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">10</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">  </span></span><span style="--0:#ABB2BF">position: </span><span style="--0:#D19A66">absolute</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">11</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">  </span></span><span style="--0:#ABB2BF">
296inset: </span><span style="--0:#D19A66">0</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">12</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">  </span></span><span style="--0:#ABB2BF">opacity: </span><span style="--0:#D19A66">0</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">13</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">  </span></span><span style="--0:#ABB2BF">border-radius: </span><span style="--0:#D19A66">8</span><span style="--0:#E6888F">px</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">14</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">  </span></span><span style="--0:#ABB2BF">box-shadow: </span><span style="--0:#D19A66">0</span><span style="--0:#ABB2BF"> </span><span style="--0:#D19A66">0</span><span style="--0:#ABB2BF"> </span><span style="--0:#D19A66">30</span><span style="--0:#E6888F">px</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">rgba</span><span style="--0:#ABB2BF">(</span><span style="--0:#D19A66">251</span><span style="--0:#ABB2BF">, </span><span style="--0:#D19A66">191</span><span style="--0:#ABB2BF">, </span><span style="--0:#D19A66">36</span><span style="--0:#ABB2BF">, </span><span style="--0:#D19A66">0.6</span><span style="--0:#ABB2BF">);</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">15</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">  </span></span><span style="--0:#ABB2BF">transition: opacity </span><span style="--0:#D19A66">0.3</span><span style="--0:#E6888F">s</span><span style="--0:#ABB2BF"> </span><span style="--0:#D19A66">ease-in-out</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">16</div></div><div class="code"><span style="--0:#ABB2BF">}</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">17</div></div><div class="code">
297</div></div><div class="ec-line"><div class="gutter"><div class="ln">18</div></div><div class="code"><span style="--0:#D19A66">.card</span><span style="--0:#56B6C2">:hover::after</span><span style="--0:#ABB2BF"> {</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">19</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">  </span></span><span style="--0:#ABB2BF">opacity: </span><span style="--0:#D19A66">1</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">20</div></div><div class="code"><span style="--0:#ABB2BF">}</span></div></div></code></pre><div class="copy"><button title="Copy to clipboard" data-copied="Copied!" data-code="/* GPU-accelerated solution */�/* triggers only Composite */��.card {�  position: relative;�}��.card::after {�  content: '';�  position: absolute;�  inset: 0;�  opacity: 0;�  border-radius: 8px;�  box-shadow: 0 0 30px rgba(251, 191, 36, 0.6);�  transition: opacity 0.3s ease-in-out;�}��.card:hover::after {�  opacity: 1;�}"><div></div></button></div></figure></div></div>
298<p>Let’s see them in action.</p>
299<astro-island uid="1P6sVX" component-url="/_astro/ShadowAnimationDemo.CQkydQmE.js" component-export="default" renderer-url="/_astro/client.OBjSpQF5.js" props="{}" ssr="" client="only" opts="{&quot;name&quot;:&quot;ShadowAnimationDemo&quot;,&quot;value&quot;:&quot;react&quot;}"></astro-island>
300<div class="relative mx-2 my-8"><div style="outline:8px solid #18181b" class="
301          absolute 
302          -left-4
303          -top-4
304          h-10 
305          w-10 
306          rounded-full 
307          bg-color-background
308          border-blue-300
309          z-10 
310          flex 
311          items-center 
312          justify-center
313          border-2
314          shadow-lg
315        "><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-hand-helping stroke-current text-blue-300"><path d="M11 12h2a2 2 0 1 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 14"></path><path d="m7 18 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91l-4.2 3.9"></path><path d="m2 13 6 6"></path></svg></div><div class="
316          relative
317          rounded-lg
318          border-2 
319          bg-zinc-900
320          border-blue-300
321          p-4
322          pt-2
323        "><div class="mb-3 flex items-center gap-2"><div class="text-md absolute -top-4 right-4 bg-color-background px-2 text-blue-300">info</div></div><div class="text-zinc-300 child-style"><p>Open DevTools and hover over the boxes above. The Performance panel will reveal green paint timing bars similar to the emulation. Fewer bars, better performance.</p></div><style>
324                .child-style * {
325                    margin: 0;
326                }
327                .child-style *:not(:last-child) {
328                   margin-bottom: 16px; /* Adjust the margin value as needed */
329                }
330            </style></div></div>
331<p>This trick isn’t just for shadows. The same pattern applies to any expensive-to-paint effect: gradients, glows, c
331omplex borders. Pre-render it on a pseudo-element, then animate <code>opacity</code>.</p>
332</section><section class="heading" data-heading-rank="2"><h2 id="layer-cake">Layer Cake</h2>
333<p>When <code>transform</code> and <code>opacity</code> skip paint, where does the work actually happen? The GPU. The browser breaks your DOM into <strong>composite layers</strong>, stored as textures in GPU memory, and lets the GPU composite them together. These layers are managed in a 3D space, allowing properties like transforms, opacity, and filters to be processed directly by the GPU.</p>
334<p>To see this in action, check out the interactive inspector below, which demonstrates how browsers handle layer compositing.</p>
335<astro-island uid="Z5xuFf" component-url="/_astro/LayersDemo.CSOnipvg.js" component-export="default" renderer-url="/_astro/client.OBjSpQF5.js" props="{}" ssr="" client="only" opts="{&quot;name&quot;:&quot;LayersDemo&quot;,&quot;value&quot;:&quot;react&quot;}"></astro-island>
336<p>Think of it like a Photoshop document. The browser separates your page into layers, hands them to the GPU as flat textures, and the GPU stamps them together every frame. Three stages:</p>
337<p><strong>Browser Stage</strong>: Identifies elements that need their own compositing layers: things with blur effects, transforms, opacity, and so on.</p>
338<p><strong>GPU Stage</strong>: Each element becomes a texture uploaded to GPU memory. Simpler shapes cost less; glass effects and large blurred surfaces cost more.</p>
339<p><strong>Final Output</strong>: The GPU composites all layers in the correct order, applying transformations and effects.</p>
340<div class="relative mx-2 my-8"><div style="outline:8px solid #18181b" class="
341          absolute 
342          -left-4
343          -top-4
344          h-10 
345          w-10 
346          rounded-full 
347          bg-color-background
348          border-amber-500
349          z-10 
350          flex 
351          items-center 
352          justify-center
353          border-2
354          shadow-lg
355        "><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-notebook-pen stroke-current text-amber-500"><path d="M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4"></path><path d="M2 6h4"></path><path d="M2 10h4"></path><path d="M2 14h4"></path><path d="M2 18h4"></path><path d="M21.378 5.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"></path></svg></div><div class="
356          relative
357          rounded-lg
358          border-2 
359          bg-zinc-900
360          border-amber-500
361          p-4
362          pt-2
363        "><div class="mb-3 flex items-center gap-2"><div class="text-md absolute -top-4 right-4 bg-color-background px-2 text-amber-500">note</div></div><div class="text-zinc-300 child-style"><p>This demo is an emulation. Actual GPU impact varies by browser, device, and implementation.</p></div><style>
364                .child-style * {
365                    margin: 0;
366                }
367                .child-style *:not(:last-child) {
368                   margin-bottom: 16px; /* Adjust the margin value as needed */
369                }
370            </style></div></div>
371<p>Each promoted layer holds a texture in GPU memory. Promote too many and you hit memory pressure, especially on mobile, where this causes stutters that are invisible on your dev machine.</p>
372<p>Use <code>will-change</code> strategically: apply it just before the animation starts and remove it as soon as it ends.</p>
373<div class="expressive-code"><figure class="frame"><figcaption class="header"></figcaption><pre tabindex="0" class="wrap" style="--ecMaxLine:50ch"><code><div class="ec-line"><div class="gutter"><div class="ln">1</div></div><div class="code"><span style="--0:#9FA3AA;--0fs:italic">// Apply just before the animation starts</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">2</div></div><div class="code"><span style="--0:#E5C07B">element</span><span style="--0:#ABB2BF">.</span><span style="--0:#61AFEF">addEventListener</span><span style="--0:#ABB2BF">(</span><span style="--0:#98C379">&#39;mouseenter&#39;</span><span style="--0:#ABB2BF">
373, () </span><span style="--0:#CD89E1">=&gt;</span><span style="--0:#ABB2BF"> {</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">3</div></div><div class="code"><span class="indent">  </span><span style="--0:#E5C07B">element</span><span style="--0:#ABB2BF">.</span><span style="--0:#E5C07B">style</span><span style="--0:#ABB2BF">.</span><span style="--0:#E6888F">willChange</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">=</span><span style="--0:#ABB2BF"> </span><span style="--0:#98C379">&#39;transform, opacity&#39;</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">4</div></div><div class="code"><span style="--0:#ABB2BF">});</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">5</div></div><div class="code">
374</div></div><div class="ec-line"><div class="gutter"><div class="ln">6</div></div><div class="code"><span style="--0:#9FA3AA;--0fs:italic">// Remove once the animation is complete</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">7</div></div><div class="code"><span style="--0:#E5C07B">element</span><span style="--0:#ABB2BF">.</span><span style="--0:#61AFEF">addEventListener</span><span style="--0:#ABB2BF">(</span><span style="--0:#98C379">&#39;transitionend&#39;</span><span style="--0:#ABB2BF">
374, () </span><span style="--0:#CD89E1">=&gt;</span><span style="--0:#ABB2BF"> {</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">8</div></div><div class="code"><span class="indent">  </span><span style="--0:#E5C07B">element</span><span style="--0:#ABB2BF">.</span><span style="--0:#E5C07B">style</span><span style="--0:#ABB2BF">.</span><span style="--0:#E6888F">willChange</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">=</span><span style="--0:#ABB2BF"> </span><span style="--0:#98C379">&#39;auto&#39;</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">9</div></div><div class="code"><span style="--0:#ABB2BF">}, { </span><span style="--0:#E6888F">once</span><span style="--0:#ABB2BF">: </span><span style="--0:#D19A66">true</span><span style="--0:#ABB2BF"> });</span></div></div></code></pre><div class="copy"><button title="Copy to clipboard" data-copied="Copied!" data-code="// Apply just before the animation starts�element.addEventListener('mouseenter', () => {�  element.style.willChange = 'transform, opacity';�});��// Remove once the animation is complete�element.addEventListener('transitionend', () => {�  element.style.willChange = 'auto';�}, { once: true });"><div></div></button></div></figure></div>
375</section><section class="heading" data-heading-rank="2"><h2 id="flip-it">FLIP It</h2>
376<p>What about animating things that change layout, like expanding a card or reordering a list? Animating <code>width</code>, <code>height</code>, or <code>top</code>/<code>left</code> directly triggers Layout on every frame.</p>
377<p>FLIP (First Last Invert Play) solves this. Measure where an element <strong>is</strong> and where it’s <strong>going</strong>, then use <code>transform</code> to fake the starting position and animate back to the final one. The layout calculation happens exactly once; the rest is compositor-only.</p>
378<astro-island uid="2p0zLd" component-url="/_astro/FlipTechnique.CcQpVScR.js" component-export="default" renderer-url="/_astro/client.OBjSpQF5.js" props="{}" ssr="" client="only" opts="{&quot;name&quot;:&quot;FlipDemo&quot;,&quot;value&quot;:&quot;react&quot;}"></astro-island>
379<div class="expressive-code"><figure class="frame"><figcaption class="header"></figcaption><pre tabindex="0" class="wrap" style="--ecMaxLine:80ch"><code><div class="ec-line"><div class="gutter"><div class="ln">1</div></div><div class="code"><span style="--0:#CD89E1">const</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">ref</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">=</span><span style="--0:#ABB2BF"> </span><span style="--0:#61AFEF">useRef</span><span style="--0:#ABB2BF">(</span><span style="--0:#D19A66">null</span><span style="--0:#ABB2BF">);</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">2</div></div><div class="code"><span style="--0:#CD89E1">const</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">firstRect</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">=</span><span style="--0:#ABB2BF"> </span><span style="--0:#61AFEF">useRef</span><span style="--0:#ABB2BF">(</span><span style="--0:#D19A66">null</span><span style="--0:#ABB2BF">);</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">3</div></div><div class="code"><span style="--0:#CD89E1">const</span><span style="--0:#ABB2BF"> [</span><span style="--0:#E5C07B">trigger</span><span style="--0:#ABB2BF">, </span><span style="--0:#E5C07B">setTrigger</span><span style="--0:#ABB2BF">] </span><span style="--0:#56B6C2">=</span><span style="--0:#ABB2BF"> </span><span style="--0:#61AFEF">useState</span><span style="--0:#ABB2BF">(</span><span style="--0:#D19A66">0</span><span style="--0:#ABB2BF">);</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">4</div></div><div class="code">
380</div></div><div class="ec-line"><div class="gutter"><div class="ln">5</div></div><div class="code"><span style="--0:#CD89E1">function</span><span style="--0:#ABB2BF"> </span><span style="--0:#61AFEF">
380onMove</span><span style="--0:#ABB2BF">() {</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">6</div></div><div class="code"><span class="indent">  </span><span style="--0:#9FA3AA;--0fs:italic">// First: record position before the change</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">7</div></div><div class="code"><span class="indent">  </span><span style="--0:#E5C07B">firstRect</span><span style="--0:#ABB2BF">.</span><span style="--0:#E6888F">current</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">=</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">ref</span><span style="--0:#ABB2BF">.</span><span style="--0:#E5C07B">current</span><span style="--0:#ABB2BF">.</span><span style="--0:#61AFEF">getBoundingClientRect</span><span style="--0:#ABB2BF">();</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">8</div></div><div class="code">
381</div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">9</div></div><div class="code"><span class="indent">  </span><span style="--0:#9FA3AA;--0fs:italic">// Last: commit the layout change via state, then bump trigger</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">10</div></div><div class="code"><span class="indent">  </span><span style="--0:#61AFEF">setPosition</span><span style="--0:#ABB2BF">(</span><span style="--0:#E6888F;--0fs:italic">p</span><span style="--0:#ABB2BF"> </span><span style="--0:#CD89E1">=&gt;</span><span style="--0:#ABB2BF"> </span><span style="--0:#E6888F">p</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">===</span><span style="--0:#ABB2BF"> </span><span style="--0:#98C379">&#39;A&#39;</span><span style="--0:#ABB2BF"> </span><span style="--0:#CD89E1">?</span><span style="--0:#ABB2BF"> </span><span style="--0:#98C379">&#39;B&#39;</span><span style="--0:#ABB2BF"> </span><span style="--0:#CD89E1">:</span><span style="--0:#ABB2BF"> </span><span style="--0:#98C379">&#39;A&#39;</span><span style="--0:#ABB2BF">);</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">11</div></div><div class="code"><span class="indent">  </span><span style="--0:#61AFEF">setTrigger</span><span style="--0:#ABB2BF">(</span><span style="--0:#E6888F;--0fs:italic">t</span><span style="--0:#ABB2BF"> </span><span style="--0:#CD89E1">=&gt;</span><span style="--0:#ABB2BF"> </span><span style="--0:#E6888F">t</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">+</span><span style="--0:#ABB2BF"> </span><span style="--0:#D19A66">1</span><span style="--0:#ABB2BF">);</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">12</div></div><div class="code"><span style="--0:#ABB2BF">}</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">13</div></div><div class="code">
382</div></div><div class="ec-line"><div class="gutter"><div class="ln">14</div></div><div class="code"><span style="--0:#9FA3AA;--0fs:italic">// Invert + Play: runs after React commits new position to the DOM, before paint</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">15</div></div><div class="code"><span style="--0:#61AFEF">useLayoutEffect</span><span style="--0:#ABB2BF">(() </span><span style="--0:#CD89E1">=&gt;</span><span style="--0:#ABB2BF"> {</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">16</div></div><div class="code"><span class="indent">  </span><span style="--0:#CD89E1">if</span><span style="--0:#ABB2BF"> (</span><span style="--0:#56B6C2">!</span><span style="--0:#E5C07B">firstRect</span><span style="--0:#ABB2BF">.</span><span style="--0:#E6888F">current</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">||</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">!</span><span style="--0:#E5C07B">ref</span><span style="--0:#ABB2BF">.</span><span style="--0:#E6888F">current</span><span style="--0:#ABB2BF">) </span><span style="--0:#CD89E1">return</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">17</div></div><div class="code"><span class="indent">  </span><span style="--0:#CD89E1">const</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">first</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">=</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">firstRect</span><span style="--0:#ABB2BF">.</span><span style="--0:#E6888F">current</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">18</div></div><div class="code"><span class="indent">  </span><span style="--0:#CD89E1">const</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">last</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">=</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">ref</span><span style="--0:#ABB2BF">.</span><span style="--0:#E5C07B">current</span><span style="--0:#ABB2BF">.</span><span style="--0:#61AFEF">getBoundingClientRect</span><span style="--0:#ABB2BF">();</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">19</div></div><div class="code"><span class="indent">  </span><span style="--0:#E5C07B">firstRect</span><span style="--0:#ABB2BF">.</span><span style="--0:#E6888F">current</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">=</span><span style="--0:#ABB2BF"> </span><span style="--0:#D19A66">null</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">20</div></div><div class="code">
383</div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">21</div></div><div class="code"><span class="indent">  </span><span style="--0:#CD89E1">const</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">dx</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">=</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">first</span><span style="--0:#ABB2BF">.</span><span style="--0:#E6888F">left</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">-</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">last</span><span style="--0:#ABB2BF">.</span><span style="--0:#E6888F">left</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">22</div></div><div class="code"><span class="indent">  </span><span style="--0:#CD89E1">const</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">dy</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">=</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">first</span><span style="--0:#ABB2BF">.</span><span style="--0:#E6888F">top</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">-</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">last</span><span style="--0:#ABB2BF">.</span><span style="--0:#E6888F">top</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">23</div></div><div class="code">
384</div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">24</div></div><div class="code"><span class="indent">  </span><span style="--0:#E5C07B">ref</span><span style="--0:#ABB2BF">.</span><span style="--0:#E5C07B">current</span><span style="--0:#ABB2BF">.</span><span style="--0:#61AFEF">animate</span><span style="--0:#ABB2BF">([</span></div></div><div class="ec-line" style="--ecIndent:4ch"><div class="gutter"><div class="ln">25</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">    </span></span><span style="--0:#ABB2BF">{ </span><span style="--0:#E6888F">transform</span><span style="--0:#ABB2BF">: </span><span style="--0:#98C379">`translate(</span><span style="--0:#CD89E1">${</span><span style="--0:#E6888F">dx</span><span style="--0:#CD89E1">}</span><span style="--0:#98C379">px, </span><span style="--0:#CD89E1">${</span><span style="--0:#E6888F">dy</span><span style="--0:#CD89E1">}</span><span style="--0:#98C379">px)`</span><span style="--0:#ABB2BF"> },</span></div></div><div class="ec-line" style="--ecIndent:4ch"><div class="gutter"><div class="ln">26</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">    </span></span><span style="--0:#ABB2BF">{ </span><span style="--0:#E6888F">transform</span><span style="--0:#ABB2BF">: </span><span style="--0:#98C379">&#39;translate(0, 0)&#39;</span><span style="--0:#ABB2BF"> }</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">27</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">  </span></span><span style="--0:#ABB2BF">], { </span><span style="--0:#E6888F">duration</span><span style="--0:#ABB2BF">: </span><span style="--0:#D19A66">500</span><span style="--0:#ABB2BF">, </span><span style="--0:#E6888F">easing</span><span style="--0:#ABB2BF">: </span><span style="--0:#98C379">&#39;ease-out&#39;</span><span style="--0:#ABB2BF"> });</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">28</div></div><div class="code"><span style="--0:#ABB2BF">}, [</span><span style="--0:#E6888F">trigger</span><span style="--0:#ABB2BF">]);
384</span></div></div></code></pre><div class="copy"><button title="Copy to clipboard" data-copied="Copied!" data-code="const ref = useRef(null);�const firstRect = useRef(null);�const [trigger, setTrigger] = useState(0);��function onMove() {�  // First: record position before the change�  firstRect.current = ref.current.getBoundingClientRect();��  // Last: commit the layout change via state, then bump trigger�  setPosition(p => p === 'A' ? 'B' : 'A');�  setTrigger(t => t + 1);�}��// Invert + Play: runs after React commits new position to the DOM, before paint�useLayoutEffect(() => {�  if (!firstRect.current || !ref.current) return;�  const first = firstRect.current;�  const last = ref.current.getBoundingClientRect();�  firstRect.current = null;��  const dx = first.left - last.left;�  const dy = first.top - last.top;��  ref.current.animate([�    { transform: `translate(${dx}px, ${dy}px)` },�    { transform: 'translate(0, 0)' }�  ], { duration: 500, easing: 'ease-out' });�}, [trigger]);"><div></div></button></div></figure></div>
385<p>The layout change happens instantly (so we can measure the final position), then the entire visible animation runs purely via <code>transform</code> with no repeated Layout calls.</p>
386<div class="relative mx-2 my-8"><div style="outline:8px solid #18181b" class="
387          absolute 
388          -left-4
389          -top-4
390          h-10 
391          w-10 
392          rounded-full 
393          bg-color-background
394          border-blue-300
395          z-10 
396          flex 
397          items-center 
398          justify-center
399          border-2
400          shadow-lg
401        "><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-hand-helping stroke-current text-blue-300"><path d="M11 12h2a2 2 0 1 0 0-4h-3c-.6 0-1.1.2-1.4.6L3 14"></path><path d="m7 18 1.6-1.4c.3-.4.8-.6 1.4-.6h4c1.1 0 2.1-.4 2.8-1.2l4.6-4.4a2 2 0 0 0-2.75-2.91l-4.2 3.9"></path><path d="m2 13 6 6"></path></svg></div><div class="
402          relative
403          rounded-lg
404          border-2 
405          bg-zinc-900
406          border-blue-300
407          p-4
408          pt-2
409        "><div class="mb-3 flex items-center gap-2"><div class="text-md absolute -top-4 right-4 bg-color-background px-2 text-blue-300">info</div></div><div class="text-zinc-300 child-style"><p>In practice, (Framer) Motion’s <code>layout</code> prop handles all of this for you! But now you know exactly why it’s fast.</p></div><style>
410                .child-style * {
411                    margin: 0;
412                }
413                .child-style *:not(:last-child) {
414                   margin-bottom: 16px; /* Adjust the margin value as needed */
415                }
416            </style></div></div>
417</section><section class="heading" data-heading-rank="2"><h2 id="scroll-driven-animations">Scroll-Driven Animations</h2>
418<p>A super common pattern: animate something as the user scrolls, like a progress bar, an element fading in, or a parallax effect or the new Apple launch page. Instinctively most engineers reach for a <code>scroll</code> event listener and update styles in JS. The problem is this runs on the <strong>main thread</strong>, and if anything else is happening, frames get dropped.</p>
419<p>CSS Scroll-Driven Animations let you tie an animation directly to scroll position in CSS. No JS needed, no event listeners. When the animated properties are <code>transform</code> or <code>opacity</code>, the browser can run the whole thing on the compositor thread, without main thread involvement.</p>
420<div class="expressive-code"><figure class="frame"><figcaption class="header"></figcaption><pre tabindex="0" class="wrap" style="--ecMaxLine:51ch"><code><div class="ec-line"><div class="gutter"><div class="ln">1</div></div><div class="code"><span style="--0:#9FA3AA;--0fs:italic">/* A scroll-progress bar, no JavaScript required */</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">2</div></div><div class="code"><span style="--0:#CD89E1">@supports</span><span style="--0:#ABB2BF">
420 (animation-timeline: </span><span style="--0:#D19A66">scroll</span><span style="--0:#ABB2BF">()) {</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">3</div></div><div class="code"><span class="indent">  </span><span style="--0:#D19A66">.progress-bar</span><span style="--0:#ABB2BF"> {</span></div></div><div class="ec-line" style="--ecIndent:4ch"><div class="gutter"><div class="ln">4</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">    </span></span><span style="--0:#ABB2BF">position: </span><span style="--0:#D19A66">fixed</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:4ch"><div class="gutter"><div class="ln">5</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">    </span></span><span style="--0:#ABB2BF">top: </span><span style="--0:#D19A66">0</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:4ch"><div class="gutter"><div class="ln">6</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">    </span></span><span style="--0:#ABB2BF">left: </span><span style="--0:#D19A66">0</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:4ch"><div class="gutter"><div class="ln">7</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">    </span></span><span style="--0:#ABB2BF">height: </span><span style="--0:#D19A66">4</span><span style="--0:#E6888F">px</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:4ch"><div class="gutter"><div class="ln">8</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">    </span></span><span style="--0:#ABB2BF">background: </span><span style="--0:#D19A66">#6366f1</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:4ch"><div class="gutter"><div class="ln">9</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">    </span></span><span style="--0:#ABB2BF">transform-origin: </span><span style="--0:#D19A66">left</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:4ch"><div class="gutter"><div class="ln">10</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">    </span></span><span style="--0:#ABB2BF">animation: grow-bar </span><span style="--0:#D19A66">auto</span><span style="--0:#ABB2BF"> </span><span style="--0:#D19A66">linear</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:4ch"><div class="gutter"><div class="ln">11</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">    </span></span><span style="--0:#ABB2BF">animation-timeline: </span><span style="--0:#D19A66">scroll</span><span style="--0:#ABB2BF">();</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">12</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">  </span></span><span style="--0:#ABB2BF">}</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">13</div></div><div class="code">
421</div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">14</div></div><div class="code"><span class="indent">  </span><span style="--0:#CD89E1">@keyframes</span><span style="--0:#ABB2BF"> </span><span style="--0:#E6888F;--0fs:italic">grow-bar</span><span style="--0:#ABB2BF"> {</span></div></div><div class="ec-line" style="--ecIndent:4ch"><div class="gutter"><div class="ln">15</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">    </span></span><span style="--0:#ABB2BF">from { transform: </span><span style="--0:#56B6C2">scaleX</span><span style="--0:#ABB2BF">(</span><span style="--0:#D19A66">0</span><span style="--0:#ABB2BF">); }</span></div></div><div class="ec-line" style="--ecIndent:4ch"><div class="gutter"><div class="ln">16</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">    </span></span><span style="--0:#ABB2BF">to   { transform: </span><span style="--0:#56B6C2">scaleX</span><span style="--0:#ABB2BF">(</span><span style="--0:#D19A66">1</span><span style="--0:#ABB2BF">); }</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">17</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">  </span></span><span style="--0:#ABB2BF">}</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">18</div></div><div class="code"><span style="--0:#ABB2BF">}</span></div></div></code></pre><div class="copy"><button title="Copy to clipboard" data-copied="Copied!" data-code="/* A scroll-progress bar, no JavaScript required */�@supports (animation-timeline: scroll()) {�  .progress-bar {�    position: fixed;�    top: 0;�    left: 0;�    height: 4px;�    background: #6366f1;�    transform-origin: left;�    animation: grow-bar auto linear;�    animation-timeline: scroll();�  }��  @keyframes grow-bar {�    from { transform: scaleX(0); }�    to   { transform: scaleX(1); }�  }�}"><div></div></button></div></figure></div>
422<p>Compare that to the JS equivalent, which fires on every scroll event on the main thread:</p>
423<div class="expressive-code"><figure class="frame"><figcaption class="header"></figcaption><pre tabindex="0" class="wrap" style="--ecMaxLine:86ch"><code><div class="ec-line"><div class="gutter"><div class="ln">1</div></div><div class="code"><span style="--0:#E5C07B">window</span><span style="--0:#ABB2BF">.</span><span style="--0:#61AFEF">addEventListener</span><span style="--0:#ABB2BF">(</span><span style="--0:#98C379">&#39;scroll&#39;</span><span style="--0:#ABB2BF">
423, () </span><span style="--0:#CD89E1">=&gt;</span><span style="--0:#ABB2BF"> {</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">2</div></div><div class="code"><span class="indent">  </span><span style="--0:#CD89E1">const</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">scrolled</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">=</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">window</span><span style="--0:#ABB2BF">.</span><span style="--0:#E6888F">scrollY</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">/</span><span style="--0:#ABB2BF"> (</span><span style="--0:#E5C07B">document</span><span style="--0:#ABB2BF">.</span><span style="--0:#E5C07B">body</span><span style="--0:#ABB2BF">.</span><span style="--0:#E6888F">scrollHeight</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">-</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">window</span><span style="--0:#ABB2BF">.</span><span style="--0:#E6888F">innerHeight</span><span style="--0:#ABB2BF">);</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">3</div></div><div class="code"><span class="indent">  </span><span style="--0:#E5C07B">progressBar</span><span style="--0:#ABB2BF">.</span><span style="--0:#E5C07B">style</span><span style="--0:#ABB2BF">.</span><span style="--0:#E6888F">transform</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">=</span><span style="--0:#ABB2BF"> </span><span style="--0:#98C379">`scaleX(</span><span style="--0:#CD89E1">${</span><span style="--0:#E6888F">scrolled</span><span style="--0:#CD89E1">}</span><span style="--0:#98C379">)`</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">4</div></div><div class="code"><span style="--0:#ABB2BF">});</span></div></div></code></pre><div class="copy"><button title="Copy to clipboard" data-copied="Copied!" data-code="window.addEventListener('scroll', () => {�  const scrolled = window.scrollY / (document.body.scrollHeight - window.innerHeight);�  progressBar.style.transform = `scaleX(${scrolled})`;�});"><div></div></button></div></figure></div>
424<div class="relative mx-2 my-8"><div style="outline:8px solid #18181b" class="
425          absolute 
426          -left-4
427          -top-4
428          h-10 
429          w-10 
430          rounded-full 
431          bg-color-background
432          border-violet-400
433          z-10 
434          flex 
435          items-center 
436          justify-center
437          border-2
438          shadow-lg
439        "><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wand-sparkles stroke-current text-violet-400"><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72"></path><path d="m14 7 3 3"></path><path d="M5 6v4"></path><path d="M19 14v4"></path><path d="M10 2v2"></path><path d="M7 8H3"></path><path d="M21 16h-4"></path><path d="M11 3H9"></path></svg></div><div class="
440          relative
441          rounded-lg
442          border-2 
443          bg-zinc-900
444          border-violet-400
445          p-4
446          pt-2
447        "><div class="mb-3 flex items-center gap-2"><div class="text-md absolute -top-4 right-4 bg-color-background px-2 text-violet-400">trivia</div></div><div class="text-zinc-300 child-style"><p>The scroll animation you see on this page’s Navbar is implemented using this css-only pattern.</p></div><style>
448                .child-style * {
449                    margin: 0;
450                }
451                .child-style *:not(:last-child) {
452                   margin-bottom: 16px; /* Adjust the margin value as needed */
453                }
454            </style></div></div>
455<p>Same visual result, very different performance profile under load. <code>animation-timeline</code> is well-supported in Chrome, Safari and other major browsers but still experimental in Firefox at the time of writing; use the <code>@supports</code> wrapper as a progressive enhancement safety net for such cases.</p>
456</section><section class="heading" data-heading-rank="2"><h2 id="120hz-problems">120Hz Problems</h2>
457<p>Modern devices increasingly ship at <strong>90 Hz, 120 Hz, 144 Hz</strong>, or higher. The math matters:</p>
458<ul>
459<li>At <strong>60 Hz</strong>: 16.7 ms per frame</li>
460<li>At <strong>120 Hz</strong>: ~8.3 ms per frame</li>
461</ul>
462<p>Platforms like iOS (ProMotion) and Android (VRR) adapt refresh rates dynamically. If your animation can’t keep up, frames drop and motion feels uneven. JS that sailed through at 60 Hz can suddenly jank at 120 Hz.</p>
463<p>You can measure the effective refresh rate using <code>requestAnimationFrame</code> timestamps:</p>
464<div class="expressive-code"><figure class="frame"><figcaption class="header"></figcaption><pre tabindex="0" class="wrap" style="--ecMaxLine:86ch"><code><div class="ec-line"><div class="gutter"><div class="ln">1</div></div><div class="code"><span style="--0:#CD89E1">let</span><span style="--0:#ABB2BF"> </span><span style="--0:#E6888F">lastTime</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">=</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">performance</span><span style="--0:#ABB2BF">.</span><span style="--0:#61AFEF">now</span><span style="--0:#ABB2BF">();</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">2</div></div><div class="code">
465</div></div><div class="ec-line"><div class="gutter"><div class="ln">3</div></div><div class="code"><span style="--0:#61AFEF">requestAnimationFrame</span><span style="--0:#ABB2BF">(</span><span style="--0:#CD89E1">function</span><span style="--0:#ABB2BF"> </span><span style="--0:#61AFEF">measure</span><span style="--0:#ABB2BF">(</span><span style="--0:#E6888F;--0fs:italic">time</span><span style="--0:#ABB2BF">) {</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">4</div></div><div class="code"><span class="indent">  </span><span style="--0:#CD89E1">const</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">delta</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">=</span><span style="--0:#ABB2BF"> </span><span style="--0:#E6888F">time</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">-</span><span style="--0:#ABB2BF"> </span><span style="--0:#E6888F">lastTime</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">5</div></div><div class="code"><span class="indent">  </span><span style="--0:#E5C07B">console</span><span style="--0:#ABB2BF">.</span><span style="--0:#61AFEF">log</span><span style="--0:#ABB2BF">(</span><span style="--0:#98C379">`Frame delta: </span><span style="--0:#CD89E1">${</span><span style="--0:#E5C07B">delta</span><span style="--0:#ABB2BF">.</span><span style="--0:#61AFEF">toFixed</span><span style="--0:#ABB2BF">(</span><span style="--0:#D19A66">1</span><span style="--0:#ABB2BF">)</span><span style="--0:#CD89E1">}</span><span style="--0:#98C379">ms (~</span><span style="--0:#CD89E1">${</span><span style="--0:#E5C07B">Math</span><span style="--0:#ABB2BF">.</span><span style="--0:#61AFEF">round</span><span style="--0:#ABB2BF">(</span><span style="--0:#D19A66">1000</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">/</span><span style="--0:#ABB2BF"> </span><span style="--0:#E6888F">delta</span><span style="--0:#ABB2BF">)</span><span style="--0:#CD89E1">}</span><span style="--0:#98C379"> Hz)`</span><span style="--0:#ABB2BF">);</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">6</div></div><div class="code"><span class="indent">  </span><span style="--0:#E6888F">lastTime</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">=</span><span style="--0:#ABB2BF"> </span><span style="--0:#E6888F">time</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">7</div></div><div class="code"><span class="indent">  </span><span style="--0:#61AFEF">requestAnimationFrame</span><span style="--0:#ABB2BF">(</span><span style="--0:#E6888F">measure</span><span style="--0:#ABB2BF">);</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">8</div></div><div class="code"><span style="--0:#ABB2BF">});
465</span></div></div></code></pre><div class="copy"><button title="Copy to clipboard" data-copied="Copied!" data-code="let lastTime = performance.now();��requestAnimationFrame(function measure(time) {�  const delta = time - lastTime;�  console.log(`Frame delta: ${delta.toFixed(1)}ms (~${Math.round(1000 / delta)} Hz)`);�  lastTime = time;�  requestAnimationFrame(measure);�});"><div></div></button></div></figure></div>
466<p>Test on high-refresh devices when possible. Even with CPU throttling, watching frame deltas will reveal timing sensitivity that your average dev machine hides.</p>
467</section><section class="heading" data-heading-rank="2"><h2 id="prefers-reduced-motion">prefers-reduced-motion</h2>
468<p>Not every user wants motion. Some have vestibular disorders where animations cause real physical discomfort. <code>prefers-reduced-motion</code> is both good UX and, increasingly, a legal accessibility requirement.</p>
469<div class="expressive-code"><figure class="frame"><figcaption class="header"></figcaption><pre tabindex="0" class="wrap" style="--ecMaxLine:43ch"><code><div class="ec-line"><div class="gutter"><div class="ln">1</div></div><div class="code"><span style="--0:#CD89E1">@media</span><span style="--0:#ABB2BF"> (prefers-reduced-motion: reduce) {</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">2</div></div><div class="code"><span class="indent">  </span><span style="--0:#E6888F">*</span><span style="--0:#ABB2BF">,</span><span style="--0:#CD89E1"> </span><span style="--0:#E6888F">*</span><span style="--0:#56B6C2">::before</span><span style="--0:#ABB2BF">,</span><span style="--0:#CD89E1"> </span><span style="--0:#E6888F">*</span><span style="--0:#56B6C2">::after</span><span style="--0:#ABB2BF"> {</span></div></div><div class="ec-line" style="--ecIndent:4ch"><div class="gutter"><div class="ln">3</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">    </span></span><span style="--0:#ABB2BF">animation-duration: </span><span style="--0:#D19A66">0.01</span><span style="--0:#E6888F">ms</span><span style="--0:#ABB2BF"> </span><span style="--0:#CD89E1">!important</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:4ch"><div class="gutter"><div class="ln">4</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">    </span></span><span style="--0:#ABB2BF">transition-duration: </span><span style="--0:#D19A66">0.01</span><span style="--0:#E6888F">ms</span><span style="--0:#ABB2BF"> </span><span style="--0:#CD89E1">!important</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">5</div></div><div class="code"><span class="indent"><span style="--0:#ABB2BF">  </span></span><span style="--0:#ABB2BF">}</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">6</div></div><div class="code"><span style="--0:#ABB2BF">}</span></div></div></code></pre><div class="copy"><button title="Copy to clipboard" data-copied="Copied!" data-code="@media (prefers-reduced-motion: reduce) {�  *, *::before, *::after {�    animation-duration: 0.01ms !important;�    transition-duration: 0.01ms !important;�  }�}"><div></div></button></div></figure></div>
470<p>For JS-driven animations:</p>
471<div class="expressive-code"><figure class="frame"><figcaption class="header"></figcaption><pre tabindex="0" class="wrap" style="--ecMaxLine:85ch"><code><div class="ec-line"><div class="gutter"><div class="ln">1</div></div><div class="code"><span style="--0:#CD89E1">const</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">prefersReduced</span><span style="--0:#ABB2BF"> </span><span style="--0:#56B6C2">=</span><span style="--0:#ABB2BF"> </span><span style="--0:#E5C07B">window</span><span style="--0:#ABB2BF">.</span><span style="--0:#61AFEF">matchMedia</span><span style="--0:#ABB2BF">(</span><span style="--0:#98C379">&#39;(prefers-reduced-motion: reduce)&#39;</span><span style="--0:#ABB2BF">).</span><span style="--0:#E6888F">matches</span><span style="--0:#ABB2BF">;</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">2</div></div><div class="code">
472</div></div><div class="ec-line"><div class="gutter"><div class="ln">3</div></div><div class="code"><span style="--0:#CD89E1">if</span><span style="--0:#ABB2BF"> (</span><span style="--0:#56B6C2">!</span><span style="--0:#E6888F">prefersReduced</span><span style="--0:#ABB2BF">) {</span></div></div><div class="ec-line" style="--ecIndent:2ch"><div class="gutter"><div class="ln">4</div></div><div class="code"><span class="indent">  </span><span style="--0:#E5C07B">element</span><span style="--0:#ABB2BF">.</span><span style="--0:#61AFEF">animate</span><span style="--0:#ABB2BF">([</span><span style="--0:#9FA3AA;--0fs:italic">/* ... */</span><span style="--0:#ABB2BF">], { </span><span style="--0:#E6888F">duration</span><span style="--0:#ABB2BF">: </span><span style="--0:#D19A66">300</span><span style="--0:#ABB2BF"> });</span></div></div><div class="ec-line"><div class="gutter"><div class="ln">5</div></div><div class="code"><span style="--0:#ABB2BF">}</span></div></div></code></pre><div class="copy"><button title="Copy to clipboard" data-copied="Copied!" data-code="const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;��if (!prefersReduced) {�  element.animate([/* ... */], { duration: 300 });�}"><div></div></button></div></figure></div>
473<p>Josh Comeau has a reusable <a href="https://www.joshwcomeau.com/snippets/react-hooks/use-prefers-reduced-motion/"><code>
473useReducedMotion</code> hook</a> worth bookmarking.</p>
474<p>One nuance worth knowing: <strong>reduce doesn’t always mean remove</strong>. Some users rely on motion cues to understand UI state changes: a sidebar sliding in, a dialog appearing. The goal is to reduce intensity and duration, not strip all motion away.</p>
475<div class="relative mx-2 my-8"><div style="outline:8px solid #18181b" class="
476          absolute 
477          -left-4
478          -top-4
479          h-10 
480          w-10 
481          rounded-full 
482          bg-color-background
483          border-amber-500
484          z-10 
485          flex 
486          items-center 
487          justify-center
488          border-2
489          shadow-lg
490        "><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-notebook-pen stroke-current text-amber-500"><path d="M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4"></path><path d="M2 6h4"></path><path d="M2 10h4"></path><path d="M2 14h4"></path><path d="M2 18h4"></path><path d="M21.378 5.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"></path></svg></div><div class="
491          relative
492          rounded-lg
493          border-2 
494          bg-zinc-900
495          border-amber-500
496          p-4
497          pt-2
498        "><div class="mb-3 flex items-center gap-2"><div class="text-md absolute -top-4 right-4 bg-color-background px-2 text-amber-500">note</div></div><div class="text-zinc-300 child-style"><p><code>@starting-style</code> (Baseline 2024) lets you define entry animations that work without JS or class-toggling hacks, and they naturally respect <code>prefers-reduced-motion</code> when combined with the pattern above.</p></div><style>
499                .child-style * {
500                    margin: 0;
501                }
502                .child-style *:not(:last-child) {
503                   margin-bottom: 16px; /* Adjust the margin value as needed */
504                }
505            </style></div></div>
506</section><section class="heading" data-heading-rank="2"><h2 id="jank-hunting">Jank Hunting</h2>
507<p>When I can feel something’s wrong but can’t explain why, I open DevTools and check three things:</p>
508<div class="flex gap-6 items-center"><div class="w-1/2"><p><strong>1. Paint Flashing</strong>(More Tools → Rendering → Paint flashing):</p><p>Highlights regions being repainted every frame. If a large area flashes green during a hover animation, you’re likely hitting the box-shadow/gradient trap from earlier.</p></div><div class="w-1/2"><img src="/_astro/demo-paint-flashing.BvrBc8ca_Z1k5mQL.webp" srcset="/_astro/demo-paint-flashing.BvrBc8ca_1RFpqp.webp 240w, /_astro/demo-paint-flashing.BvrBc8ca_Z3dBWv.webp 540w, /_astro/demo-paint-flashing.BvrBc8ca_1XPbr9.webp 720w, /_astro/demo-paint-flashing.BvrBc8ca_ZQSmk4.webp 960w" sizes="(max-width: 360px) 240px, (max-width: 720px) 540px, (max-width: 1600px) 720px, 960px" alt="DevTools paint flashing overlay example" width="961" height="698" loading="lazy" decoding="async"></div></div>
509<div class="flex gap-6 items-center"><div class="w-1/2"><p><strong>2. Layers Panel</strong> (More Tools → Layers):</p><p>Shows every compositing layer as a 3D visualization with memory cost. If you see dozens of layers on a simple page, something is over-promoting.</p></div><div class="w-1/2"><img src="/_astro/demo-layers.C91LlgOS_Z1n00z0.webp" srcset="/_astro/demo-layers.C91LlgOS_Z214TQX.webp 240w, /_astro/demo-layers.C91LlgOS_18dby3.webp 540w, /_astro/demo-layers.C91LlgOS_Z1TU8Qe.webp 720w, /_astro/demo-layers.C91LlgOS_jxrbu.webp 960w" sizes="(max-width: 360px) 240px, (max-width: 720px) 540px, (max-width: 1600px) 720px, 960px" alt="DevTools Layers panel showing compositing layers as a 3D visualization" width="961" height="698" loading="lazy" decoding="async"></div></div>
510<div class="flex gap-6 items-center"><div class="w-1/2"><p><strong>3. Performance Recorder</strong>:</p><p>Hit record, trigger your animation, stop. In the flame chart, look for long purple (Rendering) or green (Painting) bars; those are your frame-budget killers. A 6x CPU throttle simulates a mid-range Android device.</p></div><div class="w-1/2"><img src="/_astro/demo-perf.CGqElp18_1cnS8f.webp" srcset="/_astro/demo-perf.CGqElp18_1EFc5E.webp 240w, /_astro/demo-perf.CGqElp18_ZgdPig.webp 540w, /_astro/demo-perf.CGqElp18_1KOX6o.webp 720w, /_astro/demo-perf.CGqElp18_Z14SzEO.webp 960w" sizes="(max-width: 360px) 240px, (max-width: 720px) 540px, (max-width: 1600px) 720px, 960px" alt="DevTools Performance panel flame chart showing rendering and paint bars" width="961" height="698" loading="lazy" decoding="async"></div></div>
511</section><section class="heading" data-heading-rank="2"><h2 id="closing-the-loop">Closing the Loop</h2>
512<p>If you’re only taking a few things away from this, make it these:</p>
513<ul>
514<li>Prefer <code>transform</code> + <code>opacity</code> for continuous motion.</li>
515<li>Avoid layout-triggering properties (<code>width</code>, <code>top/left</code>) while animating, use transforms or a library like Motion which use FLIP technique and skip per-frame Layout entirely.</li>
516<li>Promote layers intentionally; clean up <code>will-change</code> after animation.</li>
517<li>For scroll effects, try <code>animation-timeline</code> before reaching for a JS scroll listener.</li>
518<li>Test on high-refresh and mid-range mobile; they surface problems desktops hide.</li>
519<li>Respect <code>prefers-reduced-motion</code>.</li>
520</ul>
521<p>The rendering pipeline isn’t just animation trivia, it’s a map. Once you know 
521what Layout, Paint, and Composite actually cost, you stop guessing and start reasoning. That changes how you write CSS, how you review PRs, and what you reach for first when something feels off.</p>
522<p>Pick one interaction this week. Profile it, trace it through the pipeline, and swap one property. The gap between a janky and a fluid experience is usually smaller than you think.</p>
523<p>If this was useful, share it with someone shipping animations. And if you have a favourite technique I missed, <a href="https://bsky.app/profile/siwalik.in">let me know on BlueSky</a> or <a href="https://twitter.com/siwalikm">Twitter</a>.</p>
524</section><section class="heading" data-heading-rank="2"><h2 id="relevant-reads">Relevant Reads</h2>
525<p>If you want to go deeper, here are some reads that expand on the topics I barely touched.</p>
526<ul>
527<li><a href="https://web.dev/articles/rendering-performance">Rendering Performance</a> (web.dev) - excellent primer on pipeline stages and frame budgets. The authors also have a free course on <a href="https://www.udacity.com/course/browser-rendering-optimization--ud860">Udemy</a> which you’ll have a lot of fun learning.</li>
528<li><a href="https://developer.chrome.com/docs/css-ui/scroll-driven-animations">Scroll-Driven Animations</a> (Chrome for Developers)</li>
529<li><a href="https://aerotwist.com/blog/flip-your-animations/">FLIP Your Animations</a> (Aerotwist) - the original FLIP writeup by Paul Lewis</li>
530<li><a href="https://web.archive.org/web/20250817092307/https://dassur.ma/things/120fps/">120fps and no jank</a> (Surma) - why high-refresh fundamentally tightens budgets</li>
531<li><a href="https://www.chromium.org/developers/design-documents/gpu-accelerated-compositing-in-chrome/">GPU Accelerated Compositing in Chrome</a> from chromium.org</li>
532<li><a href="https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/CSS_JavaScript_animation_performance">CSS JS Animation Performance Guide</a> by mozilla.org</li>
533</ul>
534</section> </article> </div> <div class="post-body sticky top-20 mx-8 hidden text-color-font-secondary md:block" slot="right-extra-content"> <div class="mb-4 font-semibold text-color-font-primary">Table of Contents</div> <nav class="toc sticky pt-2 text-sm" data-astro-cid-lqte4vf2> <ul class="my-1 grid pl-2" data-astro-cid-lqte4vf2> <li class="toc-wrapper grid"> <a href="#flip-book" class="mb-[4px] py-1">Flip Book</a>  </li><li class="toc-wrapper grid"> <a href="#the-pixel-rendering-pipeline" class="mb-[4px] py-1">The Pixel Rendering Pipeline</a>  </li><li class="toc-wrapper grid"> <a href="#shadow-boxing" class="mb-[4px] py-1">Shadow Boxing</a>  </li><li class="toc-wrapper grid"> <a href="#layer-cake" class="mb-[4px] py-1">Layer Cake</a>  </li><li class="toc-wrapper grid"> <a href="#flip-it" class="mb-[4px] py-1">FLIP It</a>  </li><li class="toc-wrapper grid"> <a href="#scroll-driven-animations" class="mb-[4px] py-1">Scroll-Driven Animations</a>  </li><li class="toc-wrapper grid"> <a href="#120hz-problems" class="mb-[4px] py-1">120Hz Problems</a>  </li><li class="toc-wrapper grid"> <a href="#prefers-reduced-motion" class="mb-[4px] py-1">prefers-reduced-motion</a>  </li><li class="toc-wrapper grid"> <a href="#jank-hunting" class="mb-[4px] py-1">Jank Hunting</a>  </li><li class="toc-wrapper grid"> <a href="#closing-the-loop" class="mb-[4px] py-1">Closing the Loop</a>  </li><li class="toc-wrapper grid"> <a href="#relevant-reads" class="mb-[4px] py-1">Relevant Reads</a>  </li> </ul> <svg class="toc-progress pointer-events-none absolute -left-2 top-0 h-full w-full text-color-accent" xmlns="http://www.w3.org/2000/svg" data-astro-cid-lqte4vf2> <path class="toc-marker" fill="none" stroke="currentColor" stroke-width="4" stroke-dasharray="1 0 1000 1000" stroke-dashoffset="1" stroke-linecap="round" stroke-linejoin="round" data-astro-cid-lqte4vf2></path> </svg> </nav>   </div> </main>   <footer class="mx-4" aria-labelledby="footer-heading"> <h2 id="footer-heading" class="sr-only">Footer</h2> <div class="mx-auto">  <div class="pb-8 xl:grid xl:grid-cols-5 xl:gap-8"></div> <div class="lg:flex lg:items-center lg:justify-between"> <div class="mx-auto"> 
534<script data-astro-rerun async data-uid="242cea7e9f" src="https://siwalik-mukherjee.kit.com/242cea7e9f/index.js"></script>
534 </div> </div>  <div class="flex flex-col gap-4 border-t border-gray-700 py-4 md:flex-row md:items-center md:justify-between"> <!-- <p class="text-base text-gray-400 md:order-2 md:mt-0">&copy; {year} Siwalik Mukherjee</p> --> <div class="flex space-x-6"> <a target="_blank" href="https://github.com/siwalikm" class="text-gray-400 hover:scale-105 hover:text-gray-300"> <span class="sr-only">GitHub</span> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-github"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"></path><path d="M9 18c-4.51 2-5-2-7-2"></path></svg> </a> <a target="_blank" href="https://x.com/siwalikm" class="text-gray-400 hover:scale-105 hover:text-gray-300"> <span class="sr-only">Twitter</span> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-twitter"><path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path></svg> </a> <a target="_blank" href="https://www.linkedin.com/in/siwalikm" class="text-gray-400 hover:scale-105 hover:text-gray-300"> <span class="sr-only">LinkedIn</span> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-linkedin"><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"></path><rect width="4" height="12" x="2" y="9"></rect><circle cx="4" cy="4" r="2"></circle></svg> </a> <a target="_blank" href="https://www.youtube.com/@siwalikm" class="text-gray-400 hover:scale-105 hover:text-gray-300"> <span class="sr-only">Youtube</span> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-youtube"><path d="M2.5 17a24.12 24.12 0 0 1 0-10 2 2 0 0 1 1.4-1.4 49.56 49.56 0 0 1 16.2 0A2 2 0 0 1 21.5 7a24.12 24.12 0 0 1 0 10 2 2 0 0 1-1.4 1.4 49.55 49.55 0 0 1-16.2 0A2 2 0 0 1 2.5 17"></path><path d="m10 15 5-3-5-3z"></path></svg> </a> </div> <div class="text-gray-400">
535This is V4 of my personal site.
536<a target="_blank" href="https://v3.siwalik.in/">V3</a>
537•
538<a target="_blank" href="https://v2.siwalik.in/">V2</a>
539•
540<a target="_blank" href="https://v1.siwalik.in/">V1</a> </div> <button id="newsletter-trigger" data-formkit-toggle="242cea7e9f" class="relative overflow-hidden rounded-lg p-[1px] transition-transform duration-300 hover:scale-105 focus:scale-105"> <span class="absolute inset-[-1000%] animate-[spin_2s_linear_infinite] bg-[conic-gradient(from_90deg_at_50%_50%,#E2CBFF_0%,#393BB2_50%,#E2CBFF_100%)]"></span> <span class="inline-flex w-full cursor-pointer rounded-lg bg-color-background px-3 py-2 font-medium text-white backdrop-blur-3xl">
541Subscribe to updates
542</span> </button> </div> </div> </footer> 
542<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"ef27b272d7e946aba1b9bee153fdf131","r":1,"spa":2}' crossorigin="anonymous"></script>
542
543</body></html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.