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="{"name":"FlipbookPlayer","value":"react"}"></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="{"name":"FPSTimelineDemo","value":"react"}"></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="{"name":"RenderPipelineDemo","value":"react"}"></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">''</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="{"name":"ShadowAnimationDemo","value":"react"}"></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="{"name":"LayersDemo","value":"react"}"></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">'mouseenter'</span><span style="--0:#ABB2BF">
373, () </span><span style="--0:#CD89E1">=></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">'transform, opacity'</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">'transitionend'</span><span style="--0:#ABB2BF">
374, () </span><span style="--0:#CD89E1">=></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">'auto'</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="{"name":"FlipDemo","value":"react"}"></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">=></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">'A'</span><span style="--0:#ABB2BF"> </span><span style="--0:#CD89E1">?</span><span style="--0:#ABB2BF"> </span><span style="--0:#98C379">'B'</span><span style="--0:#ABB2BF"> </span><span style="--0:#CD89E1">:</span><span style="--0:#ABB2BF"> </span><span style="--0:#98C379">'A'</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">=></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">=></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">'translate(0, 0)'</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">'ease-out'</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">'scroll'</span><span style="--0:#ABB2BF">
423, () </span><span style="--0:#CD89E1">=></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">'(prefers-reduced-motion: reduce)'</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">© {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.