[{"data":1,"prerenderedAt":2942},["ShallowReactive",2],{"blog-css-vs-javascript-animations-performance/":3},{"id":4,"title":5,"body":6,"description":2892,"extension":2893,"faqs":2894,"image":2910,"imageAlt":2911,"imageCaption":2912,"meta":2913,"navigation":202,"path":2914,"publishedAt":2915,"readingTime":764,"references":2916,"seo":2932,"stem":2933,"tags":2934,"updatedAt":2915,"__hash__":2941},"blog/blog/css-vs-javascript-animations-performance.md","CSS vs JavaScript Animations: Which Is Faster for Core Web Vitals?",{"type":7,"value":8,"toc":2866},"minimark",[9,13,16,54,57,62,65,87,90,93,97,100,128,131,134,137,254,262,266,269,290,293,386,389,392,471,477,481,484,537,543,546,636,639,767,775,779,785,788,805,808,811,1035,1041,1044,1111,1114,1118,1124,1127,1141,1144,1221,1224,1227,1360,1363,1367,1370,1396,1399,1547,1550,1554,1557,1583,1586,1782,1788,1791,1795,1798,1801,1880,1883,1885,2036,2046,2054,2057,2080,2085,2088,2094,2105,2108,2112,2115,2118,2223,2226,2278,2282,2285,2290,2293,2296,2316,2319,2323,2326,2329,2349,2353,2356,2359,2363,2366,2370,2543,2547,2553,2695,2698,2702,2710,2768,2771,2775,2778,2781,2813,2816,2820,2823,2853,2856,2859,2862],[10,11,12],"p",{},"CSS animations feel like the safe choice. They are built into the browser, they are easy to write, and most of the time they do exactly what you expect.",[10,14,15],{},"But \"CSS is faster than JavaScript\" is a lazy rule.",[10,17,18,19,23,24,27,28,31,32,31,35,38,39,42,43,48,49,53],{},"The thing that matters most is the property you animate. Move an element with ",[20,21,22],"code",{},"transform"," or fade it with ",[20,25,26],{},"opacity",", and both CSS and JavaScript can be smooth. Animate ",[20,29,30],{},"height",", ",[20,33,34],{},"left",[20,36,37],{},"margin",", or a chunky ",[20,40,41],{},"box-shadow",", and both can make ",[44,45,47],"a",{"href":46},"/blog/improve-inp-score/","Interaction to Next Paint"," and ",[44,50,52],{"href":51},"/blog/fix-cumulative-layout-shift/","Cumulative Layout Shift"," worse.",[10,55,56],{},"This is the practical version: when CSS is enough, when JavaScript is the right tool, and which animation patterns quietly hurt Core Web Vitals.",[58,59,61],"h2",{"id":60},"the-short-answer","The short answer",[10,63,64],{},"Use this as the quick rule:",[66,67,68,72,75,81,84],"ul",{},[69,70,71],"li",{},"Simple hover, fade, slide, loading state or state transition: use CSS.",[69,73,74],{},"Drag, scroll, cursor position, physics, interruption or complex sequencing: use JavaScript.",[69,76,77,78,80],{},"Layout-changing animation: redesign it around ",[20,79,22],{}," if you can.",[69,82,83],{},"Animation during page load: keep it tiny, or wait until the main content is visible.",[69,85,86],{},"Animation during user input: profile it for INP on a real mobile device.",[10,88,89],{},"A CSS transition is not magic. A JavaScript animation is not automatically bad. The browser still has to do the same rendering work: style calculation, layout, paint, and compositing.",[10,91,92],{},"The fastest animation is the one that skips layout and paint.",[58,94,96],{"id":95},"how-browsers-render-an-animation","How browsers render an animation",[10,98,99],{},"Every visual update can pass through four stages:",[101,102,103,110,116,122],"ol",{},[69,104,105,109],{},[106,107,108],"strong",{},"Style:"," work out which CSS rules apply.",[69,111,112,115],{},[106,113,114],{},"Layout:"," calculate the size and position of elements.",[69,117,118,121],{},[106,119,120],{},"Paint:"," draw pixels for backgrounds, text, borders, shadows, and images.",[69,123,124,127],{},[106,125,126],{},"Composite:"," combine layers on screen, often on the GPU.",[10,129,130],{},"The expensive stages are layout and paint. They can affect large parts of the page, especially when one element's new size pushes other elements around.",[10,132,133],{},"The cheap stage is compositing. If the browser can move an existing layer without recalculating layout or repainting pixels, the animation has a much better chance of staying smooth.",[10,135,136],{},"That's why performance advice keeps coming back to the same two properties:",[138,139,144],"pre",{"className":140,"code":141,"language":142,"meta":143,"style":143},"language-css shiki shiki-themes github-light github-dark",".card {\n  transition: transform 200ms ease, opacity 200ms ease;\n}\n\n.card:hover {\n  transform: translateY(-4px);\n  opacity: 0.96;\n}\n","css","",[20,145,146,159,191,197,204,212,236,249],{"__ignoreMap":143},[147,148,151,155],"span",{"class":149,"line":150},"line",1,[147,152,154],{"class":153},"sScJk",".card",[147,156,158],{"class":157},"sVt8B"," {\n",[147,160,162,166,169,172,176,179,182,184,186,188],{"class":149,"line":161},2,[147,163,165],{"class":164},"sj4cs","  transition",[147,167,168],{"class":157},": transform ",[147,170,171],{"class":164},"200",[147,173,175],{"class":174},"szBVR","ms",[147,177,178],{"class":164}," ease",[147,180,181],{"class":157},", opacity ",[147,183,171],{"class":164},[147,185,175],{"class":174},[147,187,178],{"class":164},[147,189,190],{"class":157},";\n",[147,192,194],{"class":149,"line":193},3,[147,195,196],{"class":157},"}\n",[147,198,200],{"class":149,"line":199},4,[147,201,203],{"emptyLinePlaceholder":202},true,"\n",[147,205,207,210],{"class":149,"line":206},5,[147,208,209],{"class":153},".card:hover",[147,211,158],{"class":157},[147,213,215,218,221,224,227,230,233],{"class":149,"line":214},6,[147,216,217],{"class":164},"  transform",[147,219,220],{"class":157},": ",[147,222,223],{"class":164},"translateY",[147,225,226],{"class":157},"(",[147,228,229],{"class":164},"-4",[147,231,232],{"class":174},"px",[147,234,235],{"class":157},");\n",[147,237,239,242,244,247],{"class":149,"line":238},7,[147,240,241],{"class":164},"  opacity",[147,243,220],{"class":157},[147,245,246],{"class":164},"0.96",[147,248,190],{"class":157},[147,250,252],{"class":149,"line":251},8,[147,253,196],{"class":157},[10,255,256,258,259,261],{},[20,257,22],{}," moves the element visually without changing its place in the document flow. ",[20,260,26],{}," changes transparency without changing layout. Both are usually compositor-friendly.",[58,263,265],{"id":264},"the-properties-that-usually-perform-well","The properties that usually perform well",[10,267,268],{},"These are the safest animation properties:",[66,270,271,276,281,286],{},[69,272,273],{},[20,274,275],{},"transform: translate(...)",[69,277,278],{},[20,279,280],{},"transform: scale(...)",[69,282,283],{},[20,284,285],{},"transform: rotate(...)",[69,287,288],{},[20,289,26],{},[10,291,292],{},"Example: a performant slide-in panel:",[138,294,296],{"className":140,"code":295,"language":142,"meta":143,"style":143},".panel {\n  transform: translateX(100%);\n  transition: transform 250ms ease;\n}\n\n.panel[data-open=\"true\"] {\n  transform: translateX(0);\n}\n",[20,297,298,305,324,339,343,347,367,382],{"__ignoreMap":143},[147,299,300,303],{"class":149,"line":150},[147,301,302],{"class":153},".panel",[147,304,158],{"class":157},[147,306,307,309,311,314,316,319,322],{"class":149,"line":161},[147,308,217],{"class":164},[147,310,220],{"class":157},[147,312,313],{"class":164},"translateX",[147,315,226],{"class":157},[147,317,318],{"class":164},"100",[147,320,321],{"class":174},"%",[147,323,235],{"class":157},[147,325,326,328,330,333,335,337],{"class":149,"line":193},[147,327,165],{"class":164},[147,329,168],{"class":157},[147,331,332],{"class":164},"250",[147,334,175],{"class":174},[147,336,178],{"class":164},[147,338,190],{"class":157},[147,340,341],{"class":149,"line":199},[147,342,196],{"class":157},[147,344,345],{"class":149,"line":206},[147,346,203],{"emptyLinePlaceholder":202},[147,348,349,351,354,357,360,364],{"class":149,"line":214},[147,350,302],{"class":153},[147,352,353],{"class":157},"[",[147,355,356],{"class":153},"data-open",[147,358,359],{"class":174},"=",[147,361,363],{"class":362},"sZZnC","\"true\"",[147,365,366],{"class":157},"] {\n",[147,368,369,371,373,375,377,380],{"class":149,"line":238},[147,370,217],{"class":164},[147,372,220],{"class":157},[147,374,313],{"class":164},[147,376,226],{"class":157},[147,378,379],{"class":164},"0",[147,381,235],{"class":157},[147,383,384],{"class":149,"line":251},[147,385,196],{"class":157},[10,387,388],{},"The panel appears to move, but its layout box does not force the rest of the page to recalculate on every frame.",[10,390,391],{},"Compare that with this version:",[138,393,395],{"className":140,"code":394,"language":142,"meta":143,"style":143},".panel {\n  right: -320px;\n  transition: right 250ms ease;\n}\n\n.panel[data-open=\"true\"] {\n  right: 0;\n}\n",[20,396,397,403,417,435,439,443,457,467],{"__ignoreMap":143},[147,398,399,401],{"class":149,"line":150},[147,400,302],{"class":153},[147,402,158],{"class":157},[147,404,405,408,410,413,415],{"class":149,"line":161},[147,406,407],{"class":164},"  right",[147,409,220],{"class":157},[147,411,412],{"class":164},"-320",[147,414,232],{"class":174},[147,416,190],{"class":157},[147,418,419,421,423,426,429,431,433],{"class":149,"line":193},[147,420,165],{"class":164},[147,422,220],{"class":157},[147,424,425],{"class":164},"right",[147,427,428],{"class":164}," 250",[147,430,175],{"class":174},[147,432,178],{"class":164},[147,434,190],{"class":157},[147,436,437],{"class":149,"line":199},[147,438,196],{"class":157},[147,440,441],{"class":149,"line":206},[147,442,203],{"emptyLinePlaceholder":202},[147,444,445,447,449,451,453,455],{"class":149,"line":214},[147,446,302],{"class":153},[147,448,353],{"class":157},[147,450,356],{"class":153},[147,452,359],{"class":174},[147,454,363],{"class":362},[147,456,366],{"class":157},[147,458,459,461,463,465],{"class":149,"line":238},[147,460,407],{"class":164},[147,462,220],{"class":157},[147,464,379],{"class":164},[147,466,190],{"class":157},[147,468,469],{"class":149,"line":251},[147,470,196],{"class":157},[10,472,473,474,476],{},"Animating ",[20,475,425],{}," changes layout positioning. On a complex page, that can trigger repeated layout work and dropped frames.",[58,478,480],{"id":479},"the-properties-that-often-hurt-performance","The properties that often hurt performance",[10,482,483],{},"Be careful animating these:",[66,485,486,491,495,507,514,519,523,528,534],{},[69,487,488],{},[20,489,490],{},"width",[69,492,493],{},[20,494,30],{},[69,496,497,31,500,31,502,31,505],{},[20,498,499],{},"top",[20,501,425],{},[20,503,504],{},"bottom",[20,506,34],{},[69,508,509,31,511],{},[20,510,37],{},[20,512,513],{},"padding",[69,515,516],{},[20,517,518],{},"font-size",[69,520,521],{},[20,522,41],{},[69,524,525],{},[20,526,527],{},"filter",[69,529,530,533],{},[20,531,532],{},"border-radius"," on large elements",[69,535,536],{},"large gradients or background-position changes",[10,538,539,540,542],{},"They are not forbidden. A small ",[20,541,30],{}," animation in an isolated menu might be fine. But these properties are much more likely to trigger layout or paint work.",[10,544,545],{},"Bad accordion pattern:",[138,547,549],{"className":140,"code":548,"language":142,"meta":143,"style":143},".answer {\n  height: 0;\n  overflow: hidden;\n  transition: height 250ms ease;\n}\n\n.answer[data-open=\"true\"] {\n  height: 240px;\n}\n",[20,550,551,558,569,581,596,600,604,618,631],{"__ignoreMap":143},[147,552,553,556],{"class":149,"line":150},[147,554,555],{"class":153},".answer",[147,557,158],{"class":157},[147,559,560,563,565,567],{"class":149,"line":161},[147,561,562],{"class":164},"  height",[147,564,220],{"class":157},[147,566,379],{"class":164},[147,568,190],{"class":157},[147,570,571,574,576,579],{"class":149,"line":193},[147,572,573],{"class":164},"  overflow",[147,575,220],{"class":157},[147,577,578],{"class":164},"hidden",[147,580,190],{"class":157},[147,582,583,585,588,590,592,594],{"class":149,"line":199},[147,584,165],{"class":164},[147,586,587],{"class":157},": height ",[147,589,332],{"class":164},[147,591,175],{"class":174},[147,593,178],{"class":164},[147,595,190],{"class":157},[147,597,598],{"class":149,"line":206},[147,599,196],{"class":157},[147,601,602],{"class":149,"line":214},[147,603,203],{"emptyLinePlaceholder":202},[147,605,606,608,610,612,614,616],{"class":149,"line":238},[147,607,555],{"class":153},[147,609,353],{"class":157},[147,611,356],{"class":153},[147,613,359],{"class":174},[147,615,363],{"class":362},[147,617,366],{"class":157},[147,619,620,622,624,627,629],{"class":149,"line":251},[147,621,562],{"class":164},[147,623,220],{"class":157},[147,625,626],{"class":164},"240",[147,628,232],{"class":174},[147,630,190],{"class":157},[147,632,634],{"class":149,"line":633},9,[147,635,196],{"class":157},[10,637,638],{},"Better pattern:",[138,640,642],{"className":140,"code":641,"language":142,"meta":143,"style":143},".answer-inner {\n  transform: scaleY(0);\n  transform-origin: top;\n  opacity: 0;\n  transition: transform 200ms ease, opacity 200ms ease;\n}\n\n.answer[data-open=\"true\"] .answer-inner {\n  transform: scaleY(1);\n  opacity: 1;\n}\n",[20,643,644,651,666,677,687,709,713,717,736,751,762],{"__ignoreMap":143},[147,645,646,649],{"class":149,"line":150},[147,647,648],{"class":153},".answer-inner",[147,650,158],{"class":157},[147,652,653,655,657,660,662,664],{"class":149,"line":161},[147,654,217],{"class":164},[147,656,220],{"class":157},[147,658,659],{"class":164},"scaleY",[147,661,226],{"class":157},[147,663,379],{"class":164},[147,665,235],{"class":157},[147,667,668,671,673,675],{"class":149,"line":193},[147,669,670],{"class":164},"  transform-origin",[147,672,220],{"class":157},[147,674,499],{"class":164},[147,676,190],{"class":157},[147,678,679,681,683,685],{"class":149,"line":199},[147,680,241],{"class":164},[147,682,220],{"class":157},[147,684,379],{"class":164},[147,686,190],{"class":157},[147,688,689,691,693,695,697,699,701,703,705,707],{"class":149,"line":206},[147,690,165],{"class":164},[147,692,168],{"class":157},[147,694,171],{"class":164},[147,696,175],{"class":174},[147,698,178],{"class":164},[147,700,181],{"class":157},[147,702,171],{"class":164},[147,704,175],{"class":174},[147,706,178],{"class":164},[147,708,190],{"class":157},[147,710,711],{"class":149,"line":214},[147,712,196],{"class":157},[147,714,715],{"class":149,"line":238},[147,716,203],{"emptyLinePlaceholder":202},[147,718,719,721,723,725,727,729,732,734],{"class":149,"line":251},[147,720,555],{"class":153},[147,722,353],{"class":157},[147,724,356],{"class":153},[147,726,359],{"class":174},[147,728,363],{"class":362},[147,730,731],{"class":157},"] ",[147,733,648],{"class":153},[147,735,158],{"class":157},[147,737,738,740,742,744,746,749],{"class":149,"line":633},[147,739,217],{"class":164},[147,741,220],{"class":157},[147,743,659],{"class":164},[147,745,226],{"class":157},[147,747,748],{"class":164},"1",[147,750,235],{"class":157},[147,752,754,756,758,760],{"class":149,"line":753},10,[147,755,241],{"class":164},[147,757,220],{"class":157},[147,759,748],{"class":164},[147,761,190],{"class":157},[147,763,765],{"class":149,"line":764},11,[147,766,196],{"class":157},[10,768,769,770,48,772,774],{},"This avoids recalculating the height every frame. If the page layout really needs to expand, consider snapping the layout open first, then animating only the inner content with ",[20,771,22],{},[20,773,26],{},".",[58,776,778],{"id":777},"how-animations-affect-inp","How animations affect INP",[10,780,781,784],{},[44,782,783],{"href":46},"INP"," measures how quickly the page responds to user interactions. Animations hurt INP when they keep the main thread busy at the exact moment the user taps, clicks, or types.",[10,786,787],{},"Common INP problems:",[66,789,790,793,796,799,802],{},[69,791,792],{},"A menu animation runs expensive JavaScript on every frame.",[69,794,795],{},"A carousel listens to scroll and recalculates layout constantly.",[69,797,798],{},"A modal opens with a heavy blur/filter effect.",[69,800,801],{},"A button click triggers both a network request and a layout-heavy animation.",[69,803,804],{},"A React state update re-renders a large component tree during the animation.",[10,806,807],{},"The user does not care that the animation looks nice. They care that the tap feels instant.",[10,809,810],{},"A safer JavaScript animation loop looks like this:",[138,812,816],{"className":813,"code":814,"language":815,"meta":143,"style":143},"language-js shiki shiki-themes github-light github-dark","let start;\nconst duration = 200;\nconst element = document.querySelector('.drawer');\n\nfunction animate(timestamp) {\n  if (!start) start = timestamp;\n  const progress = Math.min((timestamp - start) / duration, 1);\n\n  // Only update transform. Do not read layout here.\n  element.style.transform = `translateX(${(1 - progress) * 100}%)`;\n\n  if (progress \u003C 1) {\n    requestAnimationFrame(animate);\n  }\n}\n\nrequestAnimationFrame(animate);\n","js",[20,817,818,826,842,864,868,885,904,939,943,949,982,986,1002,1011,1017,1022,1027],{"__ignoreMap":143},[147,819,820,823],{"class":149,"line":150},[147,821,822],{"class":174},"let",[147,824,825],{"class":157}," start;\n",[147,827,828,831,834,837,840],{"class":149,"line":161},[147,829,830],{"class":174},"const",[147,832,833],{"class":164}," duration",[147,835,836],{"class":174}," =",[147,838,839],{"class":164}," 200",[147,841,190],{"class":157},[147,843,844,846,849,851,854,857,859,862],{"class":149,"line":193},[147,845,830],{"class":174},[147,847,848],{"class":164}," element",[147,850,836],{"class":174},[147,852,853],{"class":157}," document.",[147,855,856],{"class":153},"querySelector",[147,858,226],{"class":157},[147,860,861],{"class":362},"'.drawer'",[147,863,235],{"class":157},[147,865,866],{"class":149,"line":199},[147,867,203],{"emptyLinePlaceholder":202},[147,869,870,873,876,878,882],{"class":149,"line":206},[147,871,872],{"class":174},"function",[147,874,875],{"class":153}," animate",[147,877,226],{"class":157},[147,879,881],{"class":880},"s4XuR","timestamp",[147,883,884],{"class":157},") {\n",[147,886,887,890,893,896,899,901],{"class":149,"line":214},[147,888,889],{"class":174},"  if",[147,891,892],{"class":157}," (",[147,894,895],{"class":174},"!",[147,897,898],{"class":157},"start) start ",[147,900,359],{"class":174},[147,902,903],{"class":157}," timestamp;\n",[147,905,906,909,912,914,917,920,923,926,929,932,935,937],{"class":149,"line":238},[147,907,908],{"class":174},"  const",[147,910,911],{"class":164}," progress",[147,913,836],{"class":174},[147,915,916],{"class":157}," Math.",[147,918,919],{"class":153},"min",[147,921,922],{"class":157},"((timestamp ",[147,924,925],{"class":174},"-",[147,927,928],{"class":157}," start) ",[147,930,931],{"class":174},"/",[147,933,934],{"class":157}," duration, ",[147,936,748],{"class":164},[147,938,235],{"class":157},[147,940,941],{"class":149,"line":251},[147,942,203],{"emptyLinePlaceholder":202},[147,944,945],{"class":149,"line":633},[147,946,948],{"class":947},"sJ8bj","  // Only update transform. Do not read layout here.\n",[147,950,951,954,956,959,961,963,966,968,971,974,977,980],{"class":149,"line":753},[147,952,953],{"class":157},"  element.style.transform ",[147,955,359],{"class":174},[147,957,958],{"class":362}," `translateX(${",[147,960,226],{"class":362},[147,962,748],{"class":164},[147,964,965],{"class":174}," -",[147,967,911],{"class":157},[147,969,970],{"class":362},") ",[147,972,973],{"class":174},"*",[147,975,976],{"class":164}," 100",[147,978,979],{"class":362},"}%)`",[147,981,190],{"class":157},[147,983,984],{"class":149,"line":764},[147,985,203],{"emptyLinePlaceholder":202},[147,987,989,991,994,997,1000],{"class":149,"line":988},12,[147,990,889],{"class":174},[147,992,993],{"class":157}," (progress ",[147,995,996],{"class":174},"\u003C",[147,998,999],{"class":164}," 1",[147,1001,884],{"class":157},[147,1003,1005,1008],{"class":149,"line":1004},13,[147,1006,1007],{"class":153},"    requestAnimationFrame",[147,1009,1010],{"class":157},"(animate);\n",[147,1012,1014],{"class":149,"line":1013},14,[147,1015,1016],{"class":157},"  }\n",[147,1018,1020],{"class":149,"line":1019},15,[147,1021,196],{"class":157},[147,1023,1025],{"class":149,"line":1024},16,[147,1026,203],{"emptyLinePlaceholder":202},[147,1028,1030,1033],{"class":149,"line":1029},17,[147,1031,1032],{"class":153},"requestAnimationFrame",[147,1034,1010],{"class":157},[10,1036,1037,1038,1040],{},"The key is not ",[20,1039,1032],{}," by itself. The key is avoiding layout reads and layout writes inside the loop.",[10,1042,1043],{},"Avoid this pattern:",[138,1045,1047],{"className":813,"code":1046,"language":815,"meta":143,"style":143},"function animate() {\n  const height = element.offsetHeight; // layout read\n  element.style.height = `${height + 4}px`; // layout write\n  requestAnimationFrame(animate);\n}\n",[20,1048,1049,1058,1073,1100,1107],{"__ignoreMap":143},[147,1050,1051,1053,1055],{"class":149,"line":150},[147,1052,872],{"class":174},[147,1054,875],{"class":153},[147,1056,1057],{"class":157},"() {\n",[147,1059,1060,1062,1065,1067,1070],{"class":149,"line":161},[147,1061,908],{"class":174},[147,1063,1064],{"class":164}," height",[147,1066,836],{"class":174},[147,1068,1069],{"class":157}," element.offsetHeight; ",[147,1071,1072],{"class":947},"// layout read\n",[147,1074,1075,1078,1080,1083,1085,1088,1091,1094,1097],{"class":149,"line":193},[147,1076,1077],{"class":157},"  element.style.height ",[147,1079,359],{"class":174},[147,1081,1082],{"class":362}," `${",[147,1084,30],{"class":157},[147,1086,1087],{"class":174}," +",[147,1089,1090],{"class":164}," 4",[147,1092,1093],{"class":362},"}px`",[147,1095,1096],{"class":157},"; ",[147,1098,1099],{"class":947},"// layout write\n",[147,1101,1102,1105],{"class":149,"line":199},[147,1103,1104],{"class":153},"  requestAnimationFrame",[147,1106,1010],{"class":157},[147,1108,1109],{"class":149,"line":206},[147,1110,196],{"class":157},[10,1112,1113],{},"Reading layout and then writing layout every frame can cause forced synchronous layout. That is exactly the kind of main-thread work that makes interactions feel slow.",[58,1115,1117],{"id":1116},"how-animations-affect-cls","How animations affect CLS",[10,1119,1120,1123],{},[44,1121,1122],{"href":51},"CLS"," measures unexpected layout movement. Animations can hurt CLS when they push content around without the user causing it.",[10,1125,1126],{},"Examples:",[66,1128,1129,1132,1135,1138],{},[69,1130,1131],{},"A banner slides in above the article after the page loads.",[69,1133,1134],{},"A cookie notice appears and pushes the main content down.",[69,1136,1137],{},"A carousel image loads without reserved dimensions.",[69,1139,1140],{},"An expanding promo block changes height after fonts or scripts load.",[10,1142,1143],{},"This is a CLS problem:",[138,1145,1147],{"className":140,"code":1146,"language":142,"meta":143,"style":143},".cookie-banner {\n  height: 0;\n  transition: height 300ms ease;\n}\n\n.cookie-banner[data-visible=\"true\"] {\n  height: 96px;\n}\n",[20,1148,1149,1156,1166,1181,1185,1189,1204,1217],{"__ignoreMap":143},[147,1150,1151,1154],{"class":149,"line":150},[147,1152,1153],{"class":153},".cookie-banner",[147,1155,158],{"class":157},[147,1157,1158,1160,1162,1164],{"class":149,"line":161},[147,1159,562],{"class":164},[147,1161,220],{"class":157},[147,1163,379],{"class":164},[147,1165,190],{"class":157},[147,1167,1168,1170,1172,1175,1177,1179],{"class":149,"line":193},[147,1169,165],{"class":164},[147,1171,587],{"class":157},[147,1173,1174],{"class":164},"300",[147,1176,175],{"class":174},[147,1178,178],{"class":164},[147,1180,190],{"class":157},[147,1182,1183],{"class":149,"line":199},[147,1184,196],{"class":157},[147,1186,1187],{"class":149,"line":206},[147,1188,203],{"emptyLinePlaceholder":202},[147,1190,1191,1193,1195,1198,1200,1202],{"class":149,"line":214},[147,1192,1153],{"class":153},[147,1194,353],{"class":157},[147,1196,1197],{"class":153},"data-visible",[147,1199,359],{"class":174},[147,1201,363],{"class":362},[147,1203,366],{"class":157},[147,1205,1206,1208,1210,1213,1215],{"class":149,"line":238},[147,1207,562],{"class":164},[147,1209,220],{"class":157},[147,1211,1212],{"class":164},"96",[147,1214,232],{"class":174},[147,1216,190],{"class":157},[147,1218,1219],{"class":149,"line":251},[147,1220,196],{"class":157},[10,1222,1223],{},"It pushes the page down after the user started reading.",[10,1225,1226],{},"Better options:",[138,1228,1230],{"className":140,"code":1229,"language":142,"meta":143,"style":143},".cookie-banner {\n  position: fixed;\n  left: 16px;\n  right: 16px;\n  bottom: 16px;\n  transform: translateY(120%);\n  transition: transform 250ms ease;\n}\n\n.cookie-banner[data-visible=\"true\"] {\n  transform: translateY(0);\n}\n",[20,1231,1232,1238,1250,1264,1276,1289,1306,1320,1324,1328,1342,1356],{"__ignoreMap":143},[147,1233,1234,1236],{"class":149,"line":150},[147,1235,1153],{"class":153},[147,1237,158],{"class":157},[147,1239,1240,1243,1245,1248],{"class":149,"line":161},[147,1241,1242],{"class":164},"  position",[147,1244,220],{"class":157},[147,1246,1247],{"class":164},"fixed",[147,1249,190],{"class":157},[147,1251,1252,1255,1257,1260,1262],{"class":149,"line":193},[147,1253,1254],{"class":164},"  left",[147,1256,220],{"class":157},[147,1258,1259],{"class":164},"16",[147,1261,232],{"class":174},[147,1263,190],{"class":157},[147,1265,1266,1268,1270,1272,1274],{"class":149,"line":199},[147,1267,407],{"class":164},[147,1269,220],{"class":157},[147,1271,1259],{"class":164},[147,1273,232],{"class":174},[147,1275,190],{"class":157},[147,1277,1278,1281,1283,1285,1287],{"class":149,"line":206},[147,1279,1280],{"class":164},"  bottom",[147,1282,220],{"class":157},[147,1284,1259],{"class":164},[147,1286,232],{"class":174},[147,1288,190],{"class":157},[147,1290,1291,1293,1295,1297,1299,1302,1304],{"class":149,"line":214},[147,1292,217],{"class":164},[147,1294,220],{"class":157},[147,1296,223],{"class":164},[147,1298,226],{"class":157},[147,1300,1301],{"class":164},"120",[147,1303,321],{"class":174},[147,1305,235],{"class":157},[147,1307,1308,1310,1312,1314,1316,1318],{"class":149,"line":238},[147,1309,165],{"class":164},[147,1311,168],{"class":157},[147,1313,332],{"class":164},[147,1315,175],{"class":174},[147,1317,178],{"class":164},[147,1319,190],{"class":157},[147,1321,1322],{"class":149,"line":251},[147,1323,196],{"class":157},[147,1325,1326],{"class":149,"line":633},[147,1327,203],{"emptyLinePlaceholder":202},[147,1329,1330,1332,1334,1336,1338,1340],{"class":149,"line":753},[147,1331,1153],{"class":153},[147,1333,353],{"class":157},[147,1335,1197],{"class":153},[147,1337,359],{"class":174},[147,1339,363],{"class":362},[147,1341,366],{"class":157},[147,1343,1344,1346,1348,1350,1352,1354],{"class":149,"line":764},[147,1345,217],{"class":164},[147,1347,220],{"class":157},[147,1349,223],{"class":164},[147,1351,226],{"class":157},[147,1353,379],{"class":164},[147,1355,235],{"class":157},[147,1357,1358],{"class":149,"line":988},[147,1359,196],{"class":157},[10,1361,1362],{},"Or reserve the space from the beginning so the layout does not shift unexpectedly.",[58,1364,1366],{"id":1365},"when-css-is-the-right-tool","When CSS is the right tool",[10,1368,1369],{},"CSS is usually best for UI state transitions:",[66,1371,1372,1375,1378,1381,1384,1387,1390,1393],{},[69,1373,1374],{},"Hover states",[69,1376,1377],{},"Focus states",[69,1379,1380],{},"Button feedback",[69,1382,1383],{},"Dropdown fades",[69,1385,1386],{},"Modal entrance/exit transitions",[69,1388,1389],{},"Skeleton shimmer effects",[69,1391,1392],{},"Simple loading indicators",[69,1394,1395],{},"Reduced-motion fallbacks",[10,1397,1398],{},"Example button interaction:",[138,1400,1402],{"className":140,"code":1401,"language":142,"meta":143,"style":143},".button {\n  transition: transform 120ms ease, opacity 120ms ease;\n}\n\n.button:hover {\n  transform: translateY(-1px);\n}\n\n.button:active {\n  transform: translateY(0) scale(0.98);\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .button {\n    transition: none;\n  }\n}\n",[20,1403,1404,1411,1433,1437,1441,1448,1465,1469,1473,1480,1504,1508,1512,1520,1527,1539,1543],{"__ignoreMap":143},[147,1405,1406,1409],{"class":149,"line":150},[147,1407,1408],{"class":153},".button",[147,1410,158],{"class":157},[147,1412,1413,1415,1417,1419,1421,1423,1425,1427,1429,1431],{"class":149,"line":161},[147,1414,165],{"class":164},[147,1416,168],{"class":157},[147,1418,1301],{"class":164},[147,1420,175],{"class":174},[147,1422,178],{"class":164},[147,1424,181],{"class":157},[147,1426,1301],{"class":164},[147,1428,175],{"class":174},[147,1430,178],{"class":164},[147,1432,190],{"class":157},[147,1434,1435],{"class":149,"line":193},[147,1436,196],{"class":157},[147,1438,1439],{"class":149,"line":199},[147,1440,203],{"emptyLinePlaceholder":202},[147,1442,1443,1446],{"class":149,"line":206},[147,1444,1445],{"class":153},".button:hover",[147,1447,158],{"class":157},[147,1449,1450,1452,1454,1456,1458,1461,1463],{"class":149,"line":214},[147,1451,217],{"class":164},[147,1453,220],{"class":157},[147,1455,223],{"class":164},[147,1457,226],{"class":157},[147,1459,1460],{"class":164},"-1",[147,1462,232],{"class":174},[147,1464,235],{"class":157},[147,1466,1467],{"class":149,"line":238},[147,1468,196],{"class":157},[147,1470,1471],{"class":149,"line":251},[147,1472,203],{"emptyLinePlaceholder":202},[147,1474,1475,1478],{"class":149,"line":633},[147,1476,1477],{"class":153},".button:active",[147,1479,158],{"class":157},[147,1481,1482,1484,1486,1488,1490,1492,1494,1497,1499,1502],{"class":149,"line":753},[147,1483,217],{"class":164},[147,1485,220],{"class":157},[147,1487,223],{"class":164},[147,1489,226],{"class":157},[147,1491,379],{"class":164},[147,1493,970],{"class":157},[147,1495,1496],{"class":164},"scale",[147,1498,226],{"class":157},[147,1500,1501],{"class":164},"0.98",[147,1503,235],{"class":157},[147,1505,1506],{"class":149,"line":764},[147,1507,196],{"class":157},[147,1509,1510],{"class":149,"line":988},[147,1511,203],{"emptyLinePlaceholder":202},[147,1513,1514,1517],{"class":149,"line":1004},[147,1515,1516],{"class":174},"@media",[147,1518,1519],{"class":157}," (prefers-reduced-motion: reduce) {\n",[147,1521,1522,1525],{"class":149,"line":1013},[147,1523,1524],{"class":153},"  .button",[147,1526,158],{"class":157},[147,1528,1529,1532,1534,1537],{"class":149,"line":1019},[147,1530,1531],{"class":164},"    transition",[147,1533,220],{"class":157},[147,1535,1536],{"class":164},"none",[147,1538,190],{"class":157},[147,1540,1541],{"class":149,"line":1024},[147,1542,1016],{"class":157},[147,1544,1545],{"class":149,"line":1029},[147,1546,196],{"class":157},[10,1548,1549],{},"CSS keeps the interaction simple. It does not require a JavaScript runtime, state updates, or extra dependencies.",[58,1551,1553],{"id":1552},"when-javascript-is-the-right-tool","When JavaScript is the right tool",[10,1555,1556],{},"Use JavaScript when the animation depends on changing input:",[66,1558,1559,1562,1565,1568,1571,1574,1577,1580],{},[69,1560,1561],{},"Drag-and-drop",[69,1563,1564],{},"Swipe gestures",[69,1566,1567],{},"Scroll-linked effects",[69,1569,1570],{},"Physics or spring motion",[69,1572,1573],{},"Interruptible animations",[69,1575,1576],{},"Timeline sequencing",[69,1578,1579],{},"Animations that depend on measured element size",[69,1581,1582],{},"Canvas or WebGL effects",[10,1584,1585],{},"Example: a drag interaction needs JavaScript because every frame depends on pointer position.",[138,1587,1589],{"className":813,"code":1588,"language":815,"meta":143,"style":143},"const card = document.querySelector('.card');\nlet dragging = false;\n\ncard.addEventListener('pointerdown', () => {\n  dragging = true;\n});\n\nwindow.addEventListener('pointermove', (event) => {\n  if (!dragging) return;\n  card.style.transform = `translate(${event.clientX}px, ${event.clientY}px)`;\n});\n\nwindow.addEventListener('pointerup', () => {\n  dragging = false;\n});\n",[20,1590,1591,1611,1625,1629,1650,1662,1667,1671,1695,1711,1743,1747,1751,1768,1778],{"__ignoreMap":143},[147,1592,1593,1595,1598,1600,1602,1604,1606,1609],{"class":149,"line":150},[147,1594,830],{"class":174},[147,1596,1597],{"class":164}," card",[147,1599,836],{"class":174},[147,1601,853],{"class":157},[147,1603,856],{"class":153},[147,1605,226],{"class":157},[147,1607,1608],{"class":362},"'.card'",[147,1610,235],{"class":157},[147,1612,1613,1615,1618,1620,1623],{"class":149,"line":161},[147,1614,822],{"class":174},[147,1616,1617],{"class":157}," dragging ",[147,1619,359],{"class":174},[147,1621,1622],{"class":164}," false",[147,1624,190],{"class":157},[147,1626,1627],{"class":149,"line":193},[147,1628,203],{"emptyLinePlaceholder":202},[147,1630,1631,1634,1637,1639,1642,1645,1648],{"class":149,"line":199},[147,1632,1633],{"class":157},"card.",[147,1635,1636],{"class":153},"addEventListener",[147,1638,226],{"class":157},[147,1640,1641],{"class":362},"'pointerdown'",[147,1643,1644],{"class":157},", () ",[147,1646,1647],{"class":174},"=>",[147,1649,158],{"class":157},[147,1651,1652,1655,1657,1660],{"class":149,"line":206},[147,1653,1654],{"class":157},"  dragging ",[147,1656,359],{"class":174},[147,1658,1659],{"class":164}," true",[147,1661,190],{"class":157},[147,1663,1664],{"class":149,"line":214},[147,1665,1666],{"class":157},"});\n",[147,1668,1669],{"class":149,"line":238},[147,1670,203],{"emptyLinePlaceholder":202},[147,1672,1673,1676,1678,1680,1683,1686,1689,1691,1693],{"class":149,"line":251},[147,1674,1675],{"class":157},"window.",[147,1677,1636],{"class":153},[147,1679,226],{"class":157},[147,1681,1682],{"class":362},"'pointermove'",[147,1684,1685],{"class":157},", (",[147,1687,1688],{"class":880},"event",[147,1690,970],{"class":157},[147,1692,1647],{"class":174},[147,1694,158],{"class":157},[147,1696,1697,1699,1701,1703,1706,1709],{"class":149,"line":633},[147,1698,889],{"class":174},[147,1700,892],{"class":157},[147,1702,895],{"class":174},[147,1704,1705],{"class":157},"dragging) ",[147,1707,1708],{"class":174},"return",[147,1710,190],{"class":157},[147,1712,1713,1716,1718,1721,1723,1725,1728,1731,1733,1735,1738,1741],{"class":149,"line":753},[147,1714,1715],{"class":157},"  card.style.transform ",[147,1717,359],{"class":174},[147,1719,1720],{"class":362}," `translate(${",[147,1722,1688],{"class":157},[147,1724,774],{"class":362},[147,1726,1727],{"class":157},"clientX",[147,1729,1730],{"class":362},"}px, ${",[147,1732,1688],{"class":157},[147,1734,774],{"class":362},[147,1736,1737],{"class":157},"clientY",[147,1739,1740],{"class":362},"}px)`",[147,1742,190],{"class":157},[147,1744,1745],{"class":149,"line":764},[147,1746,1666],{"class":157},[147,1748,1749],{"class":149,"line":988},[147,1750,203],{"emptyLinePlaceholder":202},[147,1752,1753,1755,1757,1759,1762,1764,1766],{"class":149,"line":1004},[147,1754,1675],{"class":157},[147,1756,1636],{"class":153},[147,1758,226],{"class":157},[147,1760,1761],{"class":362},"'pointerup'",[147,1763,1644],{"class":157},[147,1765,1647],{"class":174},[147,1767,158],{"class":157},[147,1769,1770,1772,1774,1776],{"class":149,"line":1013},[147,1771,1654],{"class":157},[147,1773,359],{"class":174},[147,1775,1622],{"class":164},[147,1777,190],{"class":157},[147,1779,1780],{"class":149,"line":1019},[147,1781,1666],{"class":157},[10,1783,1784,1785,1787],{},"This can perform well because it still updates ",[20,1786,22],{},". JavaScript is only controlling the value.",[10,1789,1790],{},"If you use an animation library, the same rule applies. Framer Motion, GSAP, Motion One, and anime.js can all be fast or slow depending on what they animate.",[58,1792,1794],{"id":1793},"scroll-animations-the-common-trap","Scroll animations: the common trap",[10,1796,1797],{},"Scroll animations are where teams accidentally create performance problems.",[10,1799,1800],{},"Bad pattern:",[138,1802,1804],{"className":813,"code":1803,"language":815,"meta":143,"style":143},"window.addEventListener('scroll', () => {\n  const top = window.scrollY;\n  hero.style.height = `${600 - top}px`;\n  sidebar.style.top = `${top * 0.2}px`;\n});\n",[20,1805,1806,1823,1835,1855,1876],{"__ignoreMap":143},[147,1807,1808,1810,1812,1814,1817,1819,1821],{"class":149,"line":150},[147,1809,1675],{"class":157},[147,1811,1636],{"class":153},[147,1813,226],{"class":157},[147,1815,1816],{"class":362},"'scroll'",[147,1818,1644],{"class":157},[147,1820,1647],{"class":174},[147,1822,158],{"class":157},[147,1824,1825,1827,1830,1832],{"class":149,"line":161},[147,1826,908],{"class":174},[147,1828,1829],{"class":164}," top",[147,1831,836],{"class":174},[147,1833,1834],{"class":157}," window.scrollY;\n",[147,1836,1837,1840,1842,1844,1847,1849,1851,1853],{"class":149,"line":193},[147,1838,1839],{"class":157},"  hero.style.height ",[147,1841,359],{"class":174},[147,1843,1082],{"class":362},[147,1845,1846],{"class":164},"600",[147,1848,965],{"class":174},[147,1850,1829],{"class":157},[147,1852,1093],{"class":362},[147,1854,190],{"class":157},[147,1856,1857,1860,1862,1864,1866,1869,1872,1874],{"class":149,"line":199},[147,1858,1859],{"class":157},"  sidebar.style.top ",[147,1861,359],{"class":174},[147,1863,1082],{"class":362},[147,1865,499],{"class":157},[147,1867,1868],{"class":174}," *",[147,1870,1871],{"class":164}," 0.2",[147,1873,1093],{"class":362},[147,1875,190],{"class":157},[147,1877,1878],{"class":149,"line":206},[147,1879,1666],{"class":157},[10,1881,1882],{},"This runs constantly during scroll and changes layout properties.",[10,1884,638],{},[138,1886,1888],{"className":813,"code":1887,"language":815,"meta":143,"style":143},"let latestY = 0;\nlet ticking = false;\n\nwindow.addEventListener('scroll', () => {\n  latestY = window.scrollY;\n\n  if (!ticking) {\n    requestAnimationFrame(() => {\n      hero.style.transform = `translateY(${latestY * 0.15}px)`;\n      ticking = false;\n    });\n    ticking = true;\n  }\n}, { passive: true });\n",[20,1889,1890,1904,1917,1921,1937,1946,1950,1961,1972,1994,2005,2010,2021,2025],{"__ignoreMap":143},[147,1891,1892,1894,1897,1899,1902],{"class":149,"line":150},[147,1893,822],{"class":174},[147,1895,1896],{"class":157}," latestY ",[147,1898,359],{"class":174},[147,1900,1901],{"class":164}," 0",[147,1903,190],{"class":157},[147,1905,1906,1908,1911,1913,1915],{"class":149,"line":161},[147,1907,822],{"class":174},[147,1909,1910],{"class":157}," ticking ",[147,1912,359],{"class":174},[147,1914,1622],{"class":164},[147,1916,190],{"class":157},[147,1918,1919],{"class":149,"line":193},[147,1920,203],{"emptyLinePlaceholder":202},[147,1922,1923,1925,1927,1929,1931,1933,1935],{"class":149,"line":199},[147,1924,1675],{"class":157},[147,1926,1636],{"class":153},[147,1928,226],{"class":157},[147,1930,1816],{"class":362},[147,1932,1644],{"class":157},[147,1934,1647],{"class":174},[147,1936,158],{"class":157},[147,1938,1939,1942,1944],{"class":149,"line":206},[147,1940,1941],{"class":157},"  latestY ",[147,1943,359],{"class":174},[147,1945,1834],{"class":157},[147,1947,1948],{"class":149,"line":214},[147,1949,203],{"emptyLinePlaceholder":202},[147,1951,1952,1954,1956,1958],{"class":149,"line":238},[147,1953,889],{"class":174},[147,1955,892],{"class":157},[147,1957,895],{"class":174},[147,1959,1960],{"class":157},"ticking) {\n",[147,1962,1963,1965,1968,1970],{"class":149,"line":251},[147,1964,1007],{"class":153},[147,1966,1967],{"class":157},"(() ",[147,1969,1647],{"class":174},[147,1971,158],{"class":157},[147,1973,1974,1977,1979,1982,1985,1987,1990,1992],{"class":149,"line":633},[147,1975,1976],{"class":157},"      hero.style.transform ",[147,1978,359],{"class":174},[147,1980,1981],{"class":362}," `translateY(${",[147,1983,1984],{"class":157},"latestY",[147,1986,1868],{"class":174},[147,1988,1989],{"class":164}," 0.15",[147,1991,1740],{"class":362},[147,1993,190],{"class":157},[147,1995,1996,1999,2001,2003],{"class":149,"line":753},[147,1997,1998],{"class":157},"      ticking ",[147,2000,359],{"class":174},[147,2002,1622],{"class":164},[147,2004,190],{"class":157},[147,2006,2007],{"class":149,"line":764},[147,2008,2009],{"class":157},"    });\n",[147,2011,2012,2015,2017,2019],{"class":149,"line":988},[147,2013,2014],{"class":157},"    ticking ",[147,2016,359],{"class":174},[147,2018,1659],{"class":164},[147,2020,190],{"class":157},[147,2022,2023],{"class":149,"line":1004},[147,2024,1016],{"class":157},[147,2026,2027,2030,2033],{"class":149,"line":1013},[147,2028,2029],{"class":157},"}, { passive: ",[147,2031,2032],{"class":164},"true",[147,2034,2035],{"class":157}," });\n",[10,2037,2038,2039,31,2042,2045],{},"Even better: use CSS ",[20,2040,2041],{},"position: sticky",[20,2043,2044],{},"scroll-snap",", or native CSS scroll-driven animations where browser support fits your audience.",[58,2047,2049,2050,2053],{"id":2048},"the-will-change-mistake","The ",[20,2051,2052],{},"will-change"," mistake",[10,2055,2056],{},"You may have seen this advice:",[138,2058,2060],{"className":140,"code":2059,"language":142,"meta":143,"style":143},".card {\n  will-change: transform;\n}\n",[20,2061,2062,2068,2076],{"__ignoreMap":143},[147,2063,2064,2066],{"class":149,"line":150},[147,2065,154],{"class":153},[147,2067,158],{"class":157},[147,2069,2070,2073],{"class":149,"line":161},[147,2071,2072],{"class":164},"  will-change",[147,2074,2075],{"class":157},": transform;\n",[147,2077,2078],{"class":149,"line":193},[147,2079,196],{"class":157},[10,2081,2082,2084],{},[20,2083,2052],{}," tells the browser an element is likely to change, so it can prepare optimizations ahead of time. It can help for short, important animations.",[10,2086,2087],{},"But do not add it everywhere.",[10,2089,2090,2091,2093],{},"Every promoted layer uses memory. Too many layers can make performance worse, especially on mobile. Use ",[20,2092,2052],{}," only when:",[66,2095,2096,2099,2102],{},[69,2097,2098],{},"The element definitely animates soon.",[69,2100,2101],{},"The animation is important.",[69,2103,2104],{},"You remove it afterwards if it was added dynamically.",[10,2106,2107],{},"For most hover effects, you probably do not need it.",[58,2109,2111],{"id":2110},"accessibility-respect-reduced-motion","Accessibility: respect reduced motion",[10,2113,2114],{},"Performance is not the only reason to reduce animation. Some users experience motion sensitivity or vestibular discomfort.",[10,2116,2117],{},"Add a reduced motion fallback:",[138,2119,2121],{"className":140,"code":2120,"language":142,"meta":143,"style":143},"@media (prefers-reduced-motion: reduce) {\n  *,\n  *::before,\n  *::after {\n    animation-duration: 0.01ms !important;\n    animation-iteration-count: 1 !important;\n    scroll-behavior: auto !important;\n    transition-duration: 0.01ms !important;\n  }\n}\n",[20,2122,2123,2129,2138,2147,2156,2173,2186,2200,2215,2219],{"__ignoreMap":143},[147,2124,2125,2127],{"class":149,"line":150},[147,2126,1516],{"class":174},[147,2128,1519],{"class":157},[147,2130,2131,2135],{"class":149,"line":161},[147,2132,2134],{"class":2133},"s9eBZ","  *",[147,2136,2137],{"class":157},",\n",[147,2139,2140,2142,2145],{"class":149,"line":193},[147,2141,2134],{"class":2133},[147,2143,2144],{"class":153},"::before",[147,2146,2137],{"class":157},[147,2148,2149,2151,2154],{"class":149,"line":199},[147,2150,2134],{"class":2133},[147,2152,2153],{"class":153},"::after",[147,2155,158],{"class":157},[147,2157,2158,2161,2163,2166,2168,2171],{"class":149,"line":206},[147,2159,2160],{"class":164},"    animation-duration",[147,2162,220],{"class":157},[147,2164,2165],{"class":164},"0.01",[147,2167,175],{"class":174},[147,2169,2170],{"class":174}," !important",[147,2172,190],{"class":157},[147,2174,2175,2178,2180,2182,2184],{"class":149,"line":214},[147,2176,2177],{"class":164},"    animation-iteration-count",[147,2179,220],{"class":157},[147,2181,748],{"class":164},[147,2183,2170],{"class":174},[147,2185,190],{"class":157},[147,2187,2188,2191,2193,2196,2198],{"class":149,"line":238},[147,2189,2190],{"class":164},"    scroll-behavior",[147,2192,220],{"class":157},[147,2194,2195],{"class":164},"auto",[147,2197,2170],{"class":174},[147,2199,190],{"class":157},[147,2201,2202,2205,2207,2209,2211,2213],{"class":149,"line":251},[147,2203,2204],{"class":164},"    transition-duration",[147,2206,220],{"class":157},[147,2208,2165],{"class":164},[147,2210,175],{"class":174},[147,2212,2170],{"class":174},[147,2214,190],{"class":157},[147,2216,2217],{"class":149,"line":633},[147,2218,1016],{"class":157},[147,2220,2221],{"class":149,"line":753},[147,2222,196],{"class":157},[10,2224,2225],{},"For important transitions, consider replacing movement with a simple opacity change instead of removing all feedback.",[138,2227,2229],{"className":140,"code":2228,"language":142,"meta":143,"style":143},"@media (prefers-reduced-motion: reduce) {\n  .drawer {\n    transition: opacity 120ms ease;\n    transform: none;\n  }\n}\n",[20,2230,2231,2237,2244,2259,2270,2274],{"__ignoreMap":143},[147,2232,2233,2235],{"class":149,"line":150},[147,2234,1516],{"class":174},[147,2236,1519],{"class":157},[147,2238,2239,2242],{"class":149,"line":161},[147,2240,2241],{"class":153},"  .drawer",[147,2243,158],{"class":157},[147,2245,2246,2248,2251,2253,2255,2257],{"class":149,"line":193},[147,2247,1531],{"class":164},[147,2249,2250],{"class":157},": opacity ",[147,2252,1301],{"class":164},[147,2254,175],{"class":174},[147,2256,178],{"class":164},[147,2258,190],{"class":157},[147,2260,2261,2264,2266,2268],{"class":149,"line":199},[147,2262,2263],{"class":164},"    transform",[147,2265,220],{"class":157},[147,2267,1536],{"class":164},[147,2269,190],{"class":157},[147,2271,2272],{"class":149,"line":206},[147,2273,1016],{"class":157},[147,2275,2276],{"class":149,"line":214},[147,2277,196],{"class":157},[58,2279,2281],{"id":2280},"how-to-test-animation-performance","How to test animation performance",[10,2283,2284],{},"Do not judge animation performance on your development laptop. Test it.",[2286,2287,2289],"h3",{"id":2288},"_1-record-in-chrome-devtools","1. Record in Chrome DevTools",[10,2291,2292],{},"Open DevTools → Performance, then record the interaction.",[10,2294,2295],{},"Look for:",[66,2297,2298,2301,2304,2307,2310,2313],{},[69,2299,2300],{},"Long tasks over 50ms",[69,2302,2303],{},"Recalculate Style",[69,2305,2306],{},"Layout",[69,2308,2309],{},"Paint",[69,2311,2312],{},"Dropped frames",[69,2314,2315],{},"JavaScript running during the interaction",[10,2317,2318],{},"If every frame contains layout or paint work, the animation is not compositor-friendly.",[2286,2320,2322],{"id":2321},"_2-use-the-rendering-panel","2. Use the Rendering panel",[10,2324,2325],{},"In DevTools, open More tools → Rendering.",[10,2327,2328],{},"Enable:",[66,2330,2331,2337,2343],{},[69,2332,2333,2336],{},[106,2334,2335],{},"Paint flashing"," to see what repaints.",[69,2338,2339,2342],{},[106,2340,2341],{},"Layout Shift Regions"," to catch unexpected movement.",[69,2344,2345,2348],{},[106,2346,2347],{},"Frame rendering stats"," to spot dropped frames.",[2286,2350,2352],{"id":2351},"_3-test-on-mobile","3. Test on mobile",[10,2354,2355],{},"A MacBook can hide problems that appear immediately on a mid-range Android phone. Use remote debugging or at least Lighthouse mobile throttling.",[10,2357,2358],{},"The question is not \"does it feel smooth for me?\" The question is \"does it stay responsive for the slowest device I care about?\"",[58,2360,2362],{"id":2361},"practical-fixes-for-existing-sites","Practical fixes for existing sites",[10,2364,2365],{},"If your site already has janky animations, start here:",[2286,2367,2369],{"id":2368},"replace-position-animations-with-transform","Replace position animations with transform",[138,2371,2373],{"className":140,"code":2372,"language":142,"meta":143,"style":143},"/* Before */\n.toast {\n  top: -80px;\n  transition: top 200ms ease;\n}\n\n.toast[data-visible=\"true\"] {\n  top: 16px;\n}\n\n/* After */\n.toast {\n  transform: translateY(-120%);\n  transition: transform 200ms ease;\n}\n\n.toast[data-visible=\"true\"] {\n  transform: translateY(0);\n}\n",[20,2374,2375,2380,2387,2401,2417,2421,2425,2439,2451,2455,2459,2464,2470,2487,2501,2505,2509,2523,2538],{"__ignoreMap":143},[147,2376,2377],{"class":149,"line":150},[147,2378,2379],{"class":947},"/* Before */\n",[147,2381,2382,2385],{"class":149,"line":161},[147,2383,2384],{"class":153},".toast",[147,2386,158],{"class":157},[147,2388,2389,2392,2394,2397,2399],{"class":149,"line":193},[147,2390,2391],{"class":164},"  top",[147,2393,220],{"class":157},[147,2395,2396],{"class":164},"-80",[147,2398,232],{"class":174},[147,2400,190],{"class":157},[147,2402,2403,2405,2407,2409,2411,2413,2415],{"class":149,"line":199},[147,2404,165],{"class":164},[147,2406,220],{"class":157},[147,2408,499],{"class":164},[147,2410,839],{"class":164},[147,2412,175],{"class":174},[147,2414,178],{"class":164},[147,2416,190],{"class":157},[147,2418,2419],{"class":149,"line":206},[147,2420,196],{"class":157},[147,2422,2423],{"class":149,"line":214},[147,2424,203],{"emptyLinePlaceholder":202},[147,2426,2427,2429,2431,2433,2435,2437],{"class":149,"line":238},[147,2428,2384],{"class":153},[147,2430,353],{"class":157},[147,2432,1197],{"class":153},[147,2434,359],{"class":174},[147,2436,363],{"class":362},[147,2438,366],{"class":157},[147,2440,2441,2443,2445,2447,2449],{"class":149,"line":251},[147,2442,2391],{"class":164},[147,2444,220],{"class":157},[147,2446,1259],{"class":164},[147,2448,232],{"class":174},[147,2450,190],{"class":157},[147,2452,2453],{"class":149,"line":633},[147,2454,196],{"class":157},[147,2456,2457],{"class":149,"line":753},[147,2458,203],{"emptyLinePlaceholder":202},[147,2460,2461],{"class":149,"line":764},[147,2462,2463],{"class":947},"/* After */\n",[147,2465,2466,2468],{"class":149,"line":988},[147,2467,2384],{"class":153},[147,2469,158],{"class":157},[147,2471,2472,2474,2476,2478,2480,2483,2485],{"class":149,"line":1004},[147,2473,217],{"class":164},[147,2475,220],{"class":157},[147,2477,223],{"class":164},[147,2479,226],{"class":157},[147,2481,2482],{"class":164},"-120",[147,2484,321],{"class":174},[147,2486,235],{"class":157},[147,2488,2489,2491,2493,2495,2497,2499],{"class":149,"line":1013},[147,2490,165],{"class":164},[147,2492,168],{"class":157},[147,2494,171],{"class":164},[147,2496,175],{"class":174},[147,2498,178],{"class":164},[147,2500,190],{"class":157},[147,2502,2503],{"class":149,"line":1019},[147,2504,196],{"class":157},[147,2506,2507],{"class":149,"line":1024},[147,2508,203],{"emptyLinePlaceholder":202},[147,2510,2511,2513,2515,2517,2519,2521],{"class":149,"line":1029},[147,2512,2384],{"class":153},[147,2514,353],{"class":157},[147,2516,1197],{"class":153},[147,2518,359],{"class":174},[147,2520,363],{"class":362},[147,2522,366],{"class":157},[147,2524,2526,2528,2530,2532,2534,2536],{"class":149,"line":2525},18,[147,2527,217],{"class":164},[147,2529,220],{"class":157},[147,2531,223],{"class":164},[147,2533,226],{"class":157},[147,2535,379],{"class":164},[147,2537,235],{"class":157},[147,2539,2541],{"class":149,"line":2540},19,[147,2542,196],{"class":157},[2286,2544,2546],{"id":2545},"replace-size-animations-with-reveal-patterns","Replace size animations with reveal patterns",[10,2548,2549,2550,2552],{},"Instead of animating ",[20,2551,30],{},", use a wrapper:",[138,2554,2556],{"className":140,"code":2555,"language":142,"meta":143,"style":143},".reveal {\n  overflow: clip;\n}\n\n.reveal-inner {\n  transform: translateY(-8px);\n  opacity: 0;\n  transition: transform 180ms ease, opacity 180ms ease;\n}\n\n.reveal[data-open=\"true\"] .reveal-inner {\n  transform: translateY(0);\n  opacity: 1;\n}\n",[20,2557,2558,2565,2576,2580,2584,2591,2608,2618,2641,2645,2649,2667,2681,2691],{"__ignoreMap":143},[147,2559,2560,2563],{"class":149,"line":150},[147,2561,2562],{"class":153},".reveal",[147,2564,158],{"class":157},[147,2566,2567,2569,2571,2574],{"class":149,"line":161},[147,2568,573],{"class":164},[147,2570,220],{"class":157},[147,2572,2573],{"class":164},"clip",[147,2575,190],{"class":157},[147,2577,2578],{"class":149,"line":193},[147,2579,196],{"class":157},[147,2581,2582],{"class":149,"line":199},[147,2583,203],{"emptyLinePlaceholder":202},[147,2585,2586,2589],{"class":149,"line":206},[147,2587,2588],{"class":153},".reveal-inner",[147,2590,158],{"class":157},[147,2592,2593,2595,2597,2599,2601,2604,2606],{"class":149,"line":214},[147,2594,217],{"class":164},[147,2596,220],{"class":157},[147,2598,223],{"class":164},[147,2600,226],{"class":157},[147,2602,2603],{"class":164},"-8",[147,2605,232],{"class":174},[147,2607,235],{"class":157},[147,2609,2610,2612,2614,2616],{"class":149,"line":238},[147,2611,241],{"class":164},[147,2613,220],{"class":157},[147,2615,379],{"class":164},[147,2617,190],{"class":157},[147,2619,2620,2622,2624,2627,2629,2631,2633,2635,2637,2639],{"class":149,"line":251},[147,2621,165],{"class":164},[147,2623,168],{"class":157},[147,2625,2626],{"class":164},"180",[147,2628,175],{"class":174},[147,2630,178],{"class":164},[147,2632,181],{"class":157},[147,2634,2626],{"class":164},[147,2636,175],{"class":174},[147,2638,178],{"class":164},[147,2640,190],{"class":157},[147,2642,2643],{"class":149,"line":633},[147,2644,196],{"class":157},[147,2646,2647],{"class":149,"line":753},[147,2648,203],{"emptyLinePlaceholder":202},[147,2650,2651,2653,2655,2657,2659,2661,2663,2665],{"class":149,"line":764},[147,2652,2562],{"class":153},[147,2654,353],{"class":157},[147,2656,356],{"class":153},[147,2658,359],{"class":174},[147,2660,363],{"class":362},[147,2662,731],{"class":157},[147,2664,2588],{"class":153},[147,2666,158],{"class":157},[147,2668,2669,2671,2673,2675,2677,2679],{"class":149,"line":988},[147,2670,217],{"class":164},[147,2672,220],{"class":157},[147,2674,223],{"class":164},[147,2676,226],{"class":157},[147,2678,379],{"class":164},[147,2680,235],{"class":157},[147,2682,2683,2685,2687,2689],{"class":149,"line":1004},[147,2684,241],{"class":164},[147,2686,220],{"class":157},[147,2688,748],{"class":164},[147,2690,190],{"class":157},[147,2692,2693],{"class":149,"line":1013},[147,2694,196],{"class":157},[10,2696,2697],{},"If the layout must expand, do it once, not every frame.",[2286,2699,2701],{"id":2700},"delay-non-critical-entrance-animations","Delay non-critical entrance animations",[10,2703,2704,2705,2709],{},"Do not animate half the page while the browser is trying to render the ",[44,2706,2708],{"href":2707},"/blog/how-to-improve-lcp/","Largest Contentful Paint"," element.",[138,2711,2713],{"className":813,"code":2712,"language":815,"meta":143,"style":143},"window.addEventListener('load', () => {\n  requestIdleCallback?.(() => {\n    document.documentElement.classList.add('enable-decorative-animations');\n  });\n});\n",[20,2714,2715,2732,2744,2759,2764],{"__ignoreMap":143},[147,2716,2717,2719,2721,2723,2726,2728,2730],{"class":149,"line":150},[147,2718,1675],{"class":157},[147,2720,1636],{"class":153},[147,2722,226],{"class":157},[147,2724,2725],{"class":362},"'load'",[147,2727,1644],{"class":157},[147,2729,1647],{"class":174},[147,2731,158],{"class":157},[147,2733,2734,2737,2740,2742],{"class":149,"line":161},[147,2735,2736],{"class":153},"  requestIdleCallback",[147,2738,2739],{"class":157},"?.(() ",[147,2741,1647],{"class":174},[147,2743,158],{"class":157},[147,2745,2746,2749,2752,2754,2757],{"class":149,"line":193},[147,2747,2748],{"class":157},"    document.documentElement.classList.",[147,2750,2751],{"class":153},"add",[147,2753,226],{"class":157},[147,2755,2756],{"class":362},"'enable-decorative-animations'",[147,2758,235],{"class":157},[147,2760,2761],{"class":149,"line":199},[147,2762,2763],{"class":157},"  });\n",[147,2765,2766],{"class":149,"line":206},[147,2767,1666],{"class":157},[10,2769,2770],{},"Decorative animation should not compete with the hero image, web fonts, or initial JavaScript.",[2286,2772,2774],{"id":2773},"remove-animation-from-frequently-clicked-controls","Remove animation from frequently clicked controls",[10,2776,2777],{},"If a control is used repeatedly, make it instant. A fancy 300ms animation on every tab switch can make the interface feel slower even when the frame rate is fine.",[10,2779,2780],{},"For high-frequency UI, use tiny feedback:",[138,2782,2784],{"className":140,"code":2783,"language":142,"meta":143,"style":143},".tab {\n  transition: background-color 80ms ease;\n}\n",[20,2785,2786,2793,2809],{"__ignoreMap":143},[147,2787,2788,2791],{"class":149,"line":150},[147,2789,2790],{"class":153},".tab",[147,2792,158],{"class":157},[147,2794,2795,2797,2800,2803,2805,2807],{"class":149,"line":161},[147,2796,165],{"class":164},[147,2798,2799],{"class":157},": background-color ",[147,2801,2802],{"class":164},"80",[147,2804,175],{"class":174},[147,2806,178],{"class":164},[147,2808,190],{"class":157},[147,2810,2811],{"class":149,"line":193},[147,2812,196],{"class":157},[10,2814,2815],{},"Not every interaction needs motion.",[58,2817,2819],{"id":2818},"css-vs-javascript-the-final-rule","CSS vs JavaScript: the final rule",[10,2821,2822],{},"Before adding motion, ask:",[101,2824,2825,2834,2837,2840,2843,2850],{},[69,2826,2827,2828,2830,2831,2833],{},"Does it only animate ",[20,2829,22],{}," or ",[20,2832,26],{},"? If not, can it?",[69,2835,2836],{},"Does it run during page load? If yes, can it wait?",[69,2838,2839],{},"Does it respond to user input? If yes, profile INP.",[69,2841,2842],{},"Does it move other content? If yes, check CLS.",[69,2844,2845,2846,2849],{},"Does it respect ",[20,2847,2848],{},"prefers-reduced-motion","?",[69,2851,2852],{},"Does it still feel good on a mid-range phone?",[10,2854,2855],{},"CSS is the best default for simple state transitions. JavaScript is better for motion that is dynamic, interruptible or driven by input.",[10,2857,2858],{},"Neither is fast by default. Fast animations come from boring choices: the right properties, less layout work, and testing on the sort of phone your users actually have.",[10,2860,2861],{},"If your Core Web Vitals are already borderline, treat every animation as a budget decision. The smoothest animation is often the one users barely notice.",[2863,2864,2865],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":143,"searchDepth":161,"depth":161,"links":2867},[2868,2869,2870,2871,2872,2873,2874,2875,2876,2877,2879,2880,2885,2891],{"id":60,"depth":161,"text":61},{"id":95,"depth":161,"text":96},{"id":264,"depth":161,"text":265},{"id":479,"depth":161,"text":480},{"id":777,"depth":161,"text":778},{"id":1116,"depth":161,"text":1117},{"id":1365,"depth":161,"text":1366},{"id":1552,"depth":161,"text":1553},{"id":1793,"depth":161,"text":1794},{"id":2048,"depth":161,"text":2878},"The will-change mistake",{"id":2110,"depth":161,"text":2111},{"id":2280,"depth":161,"text":2281,"children":2881},[2882,2883,2884],{"id":2288,"depth":193,"text":2289},{"id":2321,"depth":193,"text":2322},{"id":2351,"depth":193,"text":2352},{"id":2361,"depth":161,"text":2362,"children":2886},[2887,2888,2889,2890],{"id":2368,"depth":193,"text":2369},{"id":2545,"depth":193,"text":2546},{"id":2700,"depth":193,"text":2701},{"id":2773,"depth":193,"text":2774},{"id":2818,"depth":161,"text":2819},"CSS animations are not automatically faster than JavaScript. Learn which animation properties hurt INP and CLS, when to use CSS, when JavaScript is safer, and how to ship smooth 60fps UI.","md",[2895,2898,2901,2904,2907],{"question":2896,"answer":2897},"Are CSS animations faster than JavaScript animations?","Not automatically. CSS animations are usually best for simple state changes, but the property you animate matters more than the language. Transform and opacity are fast. Width, height, top, left, margin, and layout-heavy properties can be slow in both CSS and JavaScript.",{"question":2899,"answer":2900},"Which CSS properties are best for performance?","Use transform and opacity whenever possible. They can often be handled by the browser compositor without recalculating layout or repainting the page. Avoid animating width, height, top, left, margin, padding, box-shadow, and filter on large elements.",{"question":2902,"answer":2903},"Do animations affect Core Web Vitals?","Yes. Heavy animations can hurt INP by blocking the main thread. Animations that move content without reserved space can hurt CLS. Large animated elements can also delay LCP if they compete with critical rendering work during page load.",{"question":2905,"answer":2906},"When should I use JavaScript animations?","Use JavaScript when the animation depends on user input, scroll position, physics, gesture velocity, interruption, or complex sequencing. Use requestAnimationFrame or a proven animation library, and keep the animated properties compositor-friendly.",{"question":2908,"answer":2909},"How do I test if an animation is hurting performance?","Record the page in Chrome DevTools Performance panel. Look for long tasks, layout recalculation, paint work, dropped frames, and interactions with high INP. Also test on a mid-range mobile device, not only a fast laptop.","/og/css-vs-javascript-animations-performance.png","Performance guide: CSS vs JavaScript Animations: Which Is Faster for Core Web Vitals?",null,{},"/blog/css-vs-javascript-animations-performance","2026-05-29",[2917,2920,2923,2926,2929],{"title":2918,"url":2919},"CSS vs. JavaScript - Josh W Comeau","https://www.joshwcomeau.com/animation/css-vs-javascript/",{"title":2921,"url":2922},"How to create high-performance CSS animations - web.dev","https://web.dev/articles/animations-guide",{"title":2924,"url":2925},"CSS and JavaScript animation performance - MDN","https://developer.mozilla.org/en-US/docs/Web/Performance/CSS_JavaScript_animation_performance",{"title":2927,"url":2928},"Interaction to Next Paint (INP) - web.dev","https://web.dev/articles/inp",{"title":2930,"url":2931},"Cumulative Layout Shift (CLS) - web.dev","https://web.dev/articles/cls",{"title":5,"description":2892},"blog/css-vs-javascript-animations-performance",[142,2935,2936,2937,2938,2939,2940],"javascript","animations","core-web-vitals","inp","cls","performance","xDMQzK4UAuk904kxkExluhF2-qEte-aEqYKLu7crFY0",1783436339751]