Making sense of Long Animation Frames Andy Davies · #PerfNow · Oct 2025 @andydavies.me Photo by Patrick Fore on Unsplash

We’re shipping more and more JavaScript @andydavies.me

Download Size != impact document.addEventListener(“mousemove”, function() { for(var a = Date.now() + 500; Date.now() < a;) ; }); OK, so it’s a bit of a silly example but you get the idea… @andydavies.me

Runtime costs are a hidden danger @andydavies.me Photo by David Clode on Unsplash

Long Tasks API We’ve tried to measure them before… https://w3c.github.io/longtasks/ @andydavies.me

Long Task = Main Thread Task > 50ms @andydavies.me Captured at 4x slowdown in Chrome DevTools

Long Task = Main Thread Task > 50ms @andydavies.me Captured at 4x slowdown in Chrome DevTools

We can measure Long Tasks with RUM @andydavies.me SpeedCurve RUM

But we get no detail on their cause { “name”: “self”, “entryType”: “longtask”, “startTime”: 48723.60000002384, “duration”: 67, “navigationId”: “3ca0c548-7618-4423-87b7-41ae43415040”, “attribution”: [ { “name”: “unknown”, “entryType”: “taskattribution”, “startTime”: 0, “duration”: 0, “navigationId”: “3ca0c548-7618-4423-87b7-41ae43415040”, “containerType”: “window”, “containerSrc”: “”, “containerId”: “”, “containerName”: “” } ] } @andydavies.me

@andydavies.me Photo by Dušan veverkolog on Unsplash

How can Long Animation Frames help us? @andydavies.me Photo by Drew Hastings on Unsplash

Lo-AF @andydavies.me

Long Animation Frames (LoAF) ls Holidays Way Cars Multi-City Flight Flights Return Hotels One Way Holidays Cars Multi-City Flight Flights Return Hotels One Way Holidays Cars Multi-City Flight Flights Return Hotels One Way Holidays Cars Multi-City Flight Flights Return Hotels One Way Holidays Cars Multi-City Flight Flights Return Hotels One Way Hol M Fligh athrow (LHR) London, UK - Heathrow (LHR) London, UK - Heathrow (LHR) London, UK - Heathrow (LHR) London, UK - Heathrow (LHR) London, UK - Heathrow (LH Praia, Cape Verde, (RAI) Nelson Mandela, Praia, Cape Verde, (RAI) Nelson Mandela, Praia, Cape Verde, (RAI) Nelson Mandela, Praia, Cape Verde, (RAI) Nelson Mandela, Praia, Cape Verde, (RAI) Nelson Mandela, Praia, Cap 14th Nov Sat, 1st Nov – Sat 14th Nov Sat, 1st Nov – Sat 14th Nov Sat, 1st Nov – Sat 14th Nov Sat, 1st Nov – Sat 14th Nov Sat, 1st Nov – Sat 14th Nov 2 Adults, First 2 Adults, First 2 Adults, First 2 Adults, First 2 Adults, First Search Search @andydavies.me Search Search Search Search

in Cars Hotels ai Return One Way P Multi-City ght Fligh ow (LHR) a, Cape Verde, (RAI) h Flights nt Holidays g St ar ts Partial measurement of frame duration London, UK - Heathrow ( LoAF Nov earch @andydavies.me Nelson Mandela, Praia, C Sat, 1st Nov – Sat 14th No

50ms 2 Adults, First Searc

ed se in P ai P

50ms re nt e am Fr LoAF @andydavies.me nt g St ar St t ar ts Start of frame to start of Painting

LoAF ed se re P P Fr ai am nt e in nt g St ar St t ar ts Start of frame to start of Painting Paint Commit GPU @andydavies.me

@andydavies.me LoAF st st tT + t on ar ti St ra ut du yo t La e nd im eA ar yl ar e St im er tT nd ar re st Timestamps for key milestones

e st ar tT im nd eA yl st er nd re LoAF + yo La t ar St e im tT ar st Script Execution du ut ra St ti ar t on In simplified Main Thread terms… Style & Layout Paint Commit GPU @andydavies.me

e st ar tT im nd eA st yl er nd re LoAF + yo La t ar St e im tT ar st Script Execution du ut ra St ti ar t on Scripts execute during render phase too Style & Layout Paint Commit GPU requestAnimationFrame ResizeObserver IntersectionObserver AnimationEvents @andydavies.me

e st ar tT im nd eA yl st er nd re LoAF + yo La t ar St e im tT ar st Script Execution du ut ra St ti ar t on Warning: This is a simplified view! Style & Layout Paint Commit GPU • • @andydavies.me scripts can force Style & Layout browsers sometimes perform Style and Layout mid-frame

Need a refresher on the Event Loop? In the Loop, Jake Archibald https://vimeo.com/254947206

Paint Timing Mixin ta en es pr pa in tT im e ti on Ti m Adds paintTime & presentationTime – Chrome Canary only ATM LoAF https://www.w3.org/TR/paint-timing/#the-paint-timing-mixin @andydavies.me

Work @andydavies.me m pr st Style & Layout Ti en es tT ar yl st LoAF Pre-Style & Layout on ti ta e im nd eA er nd re + yo La t ar St e im tT ar st du ut ra St ti ar t on Calculate durations of internal phases Paint & Compositing

LoAFs can occur across the page lifecycle @andydavies.me Fast 4G, 4x CPU slowdown – https://trace.cafe/t/JcNXq2wneo

Normally at least one before FCP @andydavies.me

Often a LoAF intersecting DCL @andydavies.me

Often a LoAF intersecting DCL Currently… deferred scripts execute in a single task just before DCL (this may change) @andydavies.me

@andydavies.me SpeedCurve RUM

@andydavies.me SpeedCurve RUM

How do LoAFs affect visitor behaviour? @andydavies.me SpeedCurve RUM

Pages can generate many LoAF entries… 100% 75% Desktop Mobile Tablet 50% 25% 0% 0 20 40 60 80 100 120 140 160 180 200 220 240 LoAFs per Page @andydavies.me SpeedCurve RUM

Pages can generate many LoAF entries… 100% 75% Desktop Mobile Tablet 50% 25% 25 0% 0 20 40 60 80 100 120 140 160 180 200 220 240 LoAFs per Page @andydavies.me SpeedCurve RUM

Pages can generate many LoAF entries… 100% 75% Desktop Mobile Tablet 50% 25% 25 0% 0 20 40 60 60 80 100 120 140 160 180 200 220 240 LoAFs per Page @andydavies.me SpeedCurve RUM

Pages can generate many LoAF entries… 100% 75% Desktop Mobile Tablet 50% 25% 25 0% 0 20 40 60 60 100 80 100 120 140 160 180 200 220 240 LoAFs per Page @andydavies.me SpeedCurve RUM

Query entries from Performance Timeline performance.getEntriesByType(“long-animation-frame”); @andydavies.me

Observe entries via PerformanceObserver const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log(entry) } }); observer.observe({ type: ‘long-animation-frame’, buffered: true }); @andydavies.me

Available in Chromium based browsers @andydavies.me

Entries are only created for visible tabs @andydavies.me

pr fir st es U en IE ta ve ti nt on Ti Ti m m e eS ta m p Did the frame handle an interaction? LoAF @andydavies.me

pr fir st es U en IE ta ve ti nt on Ti Ti m m e eS ta m p Did the frame handle an interaction? LoAF Interaction to Next Paint Measures a wider range of interactions than INP, but also has less detail @andydavies.me

How large could Input Delay be? FCP @andydavies.me

How large could Input Delay be? If someone clicks at this point in time It won’t be handled until at least here Longest LoAF = Maximum Potential Input Delay @andydavies.me

@andydavies.me st st tT + t on ar ti St ra ut du yo t La e nd im eA ar yl ar e St im er tT nd ar re st Estimate responsiveness to User Input

@andydavies.me st st tT + t on ar ti St ra ut du yo t La e nd im eA ar yl ar e St im er tT nd ar re st Estimate responsiveness to User Input

@andydavies.me st st tT + t on ar ti St ra ut du yo t La e nd im eA ar yl ar e St im er tT nd ar re st Estimate responsiveness to User Input

e st ar tT im nd eA yl st er nd re + yo La t ar St e im tT ar st blockingDuration @andydavies.me du ut ra St ti ar t on Estimate responsiveness to User Input

Total Blocking Duration (TBD) totalBlockingDuration = 0; LoAFs .forEach(entry => { totalBlockingDuration += entry.blockingDuration }); It’s a slightly naive approach but we launched TBD in SpeedCurve as a experiment with the aiming of learning and refining in public

Seems to match Main Thread busyness ok @andydavies.me SpeedCurve RUM

Refining Total Blocking Duration FCP @andydavies.me

Exclude LoAFs linked to interactions FCP @andydavies.me

What about LoAFs before FCP? FCP @andydavies.me @andydavies.me

What about LoAFs before FCP? FCP @andydavies.me

What about LoAFs before FCP? FCP

What about LoAFs before FCP? Could have rendered at this point FCP

What about LoAFs before FCP? Could have rendered at this point FCP Currently thinking of just removing the first LoAF but… …really interested in what every one else thinks too @andydavies.me

Updated Total Block Duration calculation FCP

Updated Total Block Duration calculation totalBlockingDuration = 0; LoAFs .slice(1) .filter(entry => entry.firstUIEventTimestamp == 0) .forEach(entry => { totalBlockingDuration += entry.blockingDuration }); @andydavies.me

Default thresholds need reviewing too @andydavies.me SpeedCurve RUM

LoAF has some novel uses… Photo by Jon Tyson on Unsplash

Approximate Frame Rate Calculations LoAF LoAF LoAF FPS = 1,000ms / LoAF.duration @andydavies.me LoAF

Approximate Frame Rate Calculations LoAF LoAF LoAF FPS = > 20 @andydavies.me LoAF

LoAF gives us useful data • How many frames were delayed and how long for? • Where did the frames spend their time? • What was the longest time a visitor might wait for a response? • How long was the main thread unable to respond to user input? • What was the approximate frame rate? • And more… @andydavies.me

It can help understand JS runtime costs @andydavies.me Photo by Jay Heike on Unsplash

Example LoAF entry { “name”: “long-animation-frame”, “entryType”: “long-animation-frame”, “navigationId”: “95558f7f-38d7-4a06-b15e-67ccf1dec62e”, “startTime”: 300.39999997615814, “duration”: 1573.4000000357628, “blockingDuration”: 605.986, “renderStart”: 1540, “styleAndLayoutStart”: 1540.1000000238419, “paintTime”: 1873.800000011921, “firstUIEventTimestamp”: 0, “scripts”: [] }, @andydavies.me

Script Execution @andydavies.me e st ar tT im nd eA yl st er nd re + yo La t ar St e im tT ar st LoAF du ut ra St ti ar t on Entry for each script execution > 5ms

e nS st ex ar ec tT ut im io e im tT ar st + ta du rt * ra ti on Entry for each script execution > 5ms Script Execution *executionStart is ‘muted’ for Cross Origin scripts @andydavies.me

e nS st ex ar ec tT ut im io e im tT ar st + ta du rt * ra ti on Entry for each script execution > 5ms Script Execution Compile Duration *executionStart is ‘muted’ for Cross Origin scripts @andydavies.me

Other timing information forcedStyleAndLayoutDuration getComputedStyle & getBoundingClientRect for the browser to recalculate Style and Layout pauseDuration Synchronous XHR can force the browser to pause @andydavies.me

What script executed? “sourceURL”: “https://www.example.com/script.js”, “sourceFunctionName”: “M”, “sourceCharPosition”: 98556 ! @andydavies.me

What script executed? “sourceURL”: “https://www.example.com/script.js”, “sourceFunctionName”: “M”, “sourceCharPosition”: 98556 Top level entry point ! @andydavies.me

Why did it execute? “invoker”: “DOMWindow.onclick”, “invokerType”: “event-listener”, @andydavies.me

Aggregate the Data… Roll up the Data… sourceURL https://www.selfridges.com/static-mfe-clp/_next/static/chunks/914-ff512dbd6d857625.js https://js-cdn.dynatrace.com/jstag/164ae1b51de/bf67380nlf/fb4fc3f4b31ef9b_complete.js https://www.googletagmanager.com/gtag/js?id=AW-989335448&l=gDatalayer&cx=c&gtm=4e5am0 https://www.selfridges.com/static-mfe-clp/_next/static/chunks/937-c796c646a443351b.js https://cdn.quantummetric.com/qscripts/quantum-selfridges.js https://www.selfridges.com/static-mfe-clp/_next/static/chunks/d8ec93b9-980f704a97712473.js https://www.googletagmanager.com/gtag/js?id=DC-5921516&l=gDatalayer&cx=c&gtm=4e5am0 https://tags.tiqcdn.com/utag/selfridges/main/prod/utag.js https://tags.pw.adn.cloud/SMVEJB/activation.js https://f.monetate.net/trk/4/s/a-26b02505/p/selfridges.com/1584236768-0? mr=t1640009934&mi=%272.1452019399.1731666309291%27&cs=!t&e=! https://www.googletagmanager.com/gtag/js?id=G-R05V82D63H&l=gDatalayer https://www.selfridges.com/static-mfe-clp/_next/static/chunks/1dd3208c-191771bcb3be71a1.js https://tags.tiqcdn.com/utag/selfridges/main/prod/utag.416.js?utv=ut4.51.202210200808 https://se.monetate.net/js/2/a-26b02505/p/selfridges.com/custom.js https://analytics.tiktok.com/i18n/pixel/static/main.MWI5ZTBkMTQ4MA.js https://www.selfridges.com/GB/en/ https://cdn.attraqt.io/xo.all-2.min.js https://sb.monetate.net/img/1/p/1581/5347147.js/monetate.c.cr.js https://static.queue-it.net/script/queueconfigloader.min.js https://www.selfridges.com/static-mfe-clp/_next/static/chunks/61721e05-4efc862ff71175ec.js Total Duration (ms) Total ForcedStyleAndLayout Duration (ms) Occurrences 4,555 1,328 1,021 938 842 767 618 543 226 204 156 131 60 57 38 29 21 7 6 5 5 139 8 0 0 0 0 0 119 0 0 0 0 0 0 0 0 0 0 0 0 0 30 18 2 1 10 3 1 5 2 1 2 6 1 2 3 1 1 1 1 1 1

Aggregate the Data… Roll up the Data… sourceURL https://www.selfridges.com/static-mfe-clp/_next/static/chunks/914-ff512dbd6d857625.js https://js-cdn.dynatrace.com/jstag/164ae1b51de/bf67380nlf/fb4fc3f4b31ef9b_complete.js https://www.googletagmanager.com/gtag/js?id=AW-989335448&l=gDatalayer&cx=c&gtm=4e5am0 https://www.selfridges.com/static-mfe-clp/_next/static/chunks/937-c796c646a443351b.js https://cdn.quantummetric.com/qscripts/quantum-selfridges.js https://www.selfridges.com/static-mfe-clp/_next/static/chunks/d8ec93b9-980f704a97712473.js https://www.googletagmanager.com/gtag/js?id=DC-5921516&l=gDatalayer&cx=c&gtm=4e5am0 https://tags.tiqcdn.com/utag/selfridges/main/prod/utag.js https://tags.pw.adn.cloud/SMVEJB/activation.js https://f.monetate.net/trk/4/s/a-26b02505/p/selfridges.com/1584236768-0? mr=t1640009934&mi=%272.1452019399.1731666309291%27&cs=!t&e=! https://www.googletagmanager.com/gtag/js?id=G-R05V82D63H&l=gDatalayer https://www.selfridges.com/static-mfe-clp/_next/static/chunks/1dd3208c-191771bcb3be71a1.js https://tags.tiqcdn.com/utag/selfridges/main/prod/utag.416.js?utv=ut4.51.202210200808 https://se.monetate.net/js/2/a-26b02505/p/selfridges.com/custom.js https://analytics.tiktok.com/i18n/pixel/static/main.MWI5ZTBkMTQ4MA.js https://www.selfridges.com/GB/en/ https://cdn.attraqt.io/xo.all-2.min.js https://sb.monetate.net/img/1/p/1581/5347147.js/monetate.c.cr.js https://static.queue-it.net/script/queueconfigloader.min.js https://www.selfridges.com/static-mfe-clp/_next/static/chunks/61721e05-4efc862ff71175ec.js Total Duration (ms) Total ForcedStyleAndLayout Duration (ms) Occurrences 4,555 1,328 1,021 938 842 767 618 543 226 204 156 131 60 57 38 29 21 7 6 5 5 139 8 0 0 0 0 0 119 0 0 0 0 0 0 0 0 0 0 0 0 0 30 18 2 1 10 3 1 5 2 1 2 6 1 2 3 1 1 1 1 1 1

Are 3rd-party tags really the problem? Total Script Duration (ms) @andydavies.me 1st Party 3rd Party 6,446 5,111 Captured at 4x slowdown in Chrome DevTools

May generate many entries per page CPU Slowdown 0x 4x 6x LOAF Entries 21 32 37 ScriptTiming Entries 31 39 47 Example only: @andydavies.me Real world values will depend on device CPU, network connectivity and duration of observation

Helps answer questions such as • Which scripts have the most impact on visitors experience? • Which scripts are forcing style and layout operations? • Which scripts delay FCP or LCP or affect a visitor completing a task? • Are my 1st Party or 3rd-party scripts a problem? @andydavies.me

LoAF can help diagnose slow interactions @andydavies.me Photo by Nik on Unsplash

Interaction to Next Paint (INP) Time between a visitor interacting and the next frame being presented Flights Return Hotels Holidays One Way Cars Multi-City Return Hotels One Way Holidays Cars Multi-City London, UK - Heathrow (LHR) London, UK - Heathrow (LHR) Nelson Mandela, Praia, Cape Verde, (RAI) Nelson Mandela, Praia, Cape Verde, (RAI) Sat, 1st Nov – Sat 14th Nov Sat, 1st Nov – Sat 14th Nov 2 Adults, First 2 Adults, First Flight Search @andydavies.me Flights Flight

INP has Three Phases Input Delay Waiting for event handler to execute @andydavies.me Processing Duration Presentation Delay Event handler execution Waiting for new frame to be presented

INP has Three Phases Input Delay Other Tasks @andydavies.me Processing Duration Presentation Delay Tasks related to the event handler

INP has Three Phases Input Delay Other Tasks Processing Duration Presentation Delay Tasks related to the event handler Can use DevTools to debug some slow interactions @andydavies.me

INP has Three Phases Input Delay Processing Duration Other Tasks But this depends on what’s executing when someone interacts @andydavies.me Presentation Delay Tasks related to the event handler

@andydavies.me SpeedCurve RUM

No direct way to link EventTiming & LoAF Photo by GeoJango Maps on Unsplash

Use timestamps to map scripts to phases Input Delay Processing Duration LoAF JS @andydavies.me Presentation Delay LoAF JS JS JS LoAF JS JS JS

Discard LoAFs ending after Presentation Input Delay Processing Duration LoAF JS Presentation Delay LoAF JS JS JS LoAF JS JS JS Tasks in next frame can execute once current frame is sent to GPU @andydavies.me

Discard LoAFs ending after Presentation Input Delay Processing Duration LoAF JS @andydavies.me LoAF JS JS JS JS Presentation Delay

Exclude any script execution before Input Input Delay Processing Duration LoAF JS LoAF JS Script started executing before visitor interacted JS JS JS Presentation Delay

Exclude script execution before Input Input Delay JS Processing Duration LoAF JS JS @andydavies.me LoAF JS JS JS JS Presentation Delay

Assign scripts that are left to INP phases Input Delay JS JS JS Processing Duration LoAF LoAF JS ! @andydavies.me JS JS JS Presentation Delay

Assign scripts that are left to INP phases phase InputDelay InputDelay InputDelay InputDelay ProcessingTime @andydavies.me invokerType classic-script event-listener event-listener event-listener event-listener duration (ms) 119 10 5 12 12 sourceURL https://tags.tiqcdn.com/utag/selfridges/main/prod/utag.js https://js-cdn.dynatrace.com/jstag/164ae1b51de/bf67380nlf/fb4fc3f4b31ef https://www.selfridges.com/NL/en/features/etc/designs/zg/selfridges-new/e https://www.selfridges.com/NL/en/features/etc/designs/zg/selfridges-new/e

Can help us answer questions such as: • How are scripts affecting our visitors interactions? • Which scripts have have the most impact across all interactions? • Which are the slowest interaction handlers? • Which scripts commonly delay interaction handlers @andydavies.me

Attribution Issues: Empty sourceFunction phase ID ID ID ID PT invoker invokerType entryPoint duration (ms) sourceURL https://tags.tiqcdn.com/utag/selfridges/main/prod/utag.js classic-script 119 https://tags.tiqcdn.com/utag/selfridges/main/p SCRIPT[src=//tags.tiqcdn.com/utag/selfridges/main/prod/utag.js].onload event-listener 10 #document.ontouchstart event-listener 5 https://js-cdn.dynatrace.com/jstag/164ae1b51 #document.ontouchstart event-listener 12 https://www.selfridges.com/NL/en/features/etc BODY#selfridges-app.onclick event-listener 12 https://www.selfridges.com/NL/en/features/etc Entry points may be empty or minified function names @andydavies.me

Attribution Issues: Empty sourceURLs phase ID ID ID ID PT invoker invokerType entryPoint duration (ms) sourceURL https://tags.tiqcdn.com/utag/selfridges/main/prod/utag.js classic-script 119 https://tags.tiqcdn.com/utag/selfridges/main/p SCRIPT[src=//tags.tiqcdn.com/utag/selfridges/main/prod/utag.js].onload event-listener 10 #document.ontouchstart event-listener 5 https://js-cdn.dynatrace.com/jstag/164ae1b51 #document.ontouchstart event-listener 12 https://www.selfridges.com/NL/en/features/etc BODY#selfridges-app.onclick event-listener 12 https://www.selfridges.com/NL/en/features/etc @andydavies.me Sometimes sourceURLs are empty

Attribution Issues: Wrappers invoker invokerType sourceFunctionName sourceURL Total Duration Occurences #document.ontouchend event-listener nrWrapper …com/browser-performance/newrelic.js 13 1 svg.onclick event-listener nrWrapper …com/browser-performance/newrelic.js 16 2 DOMWindow.onmousemove event-listener nrWrapper …com/browser-performance/newrelic.js 8 1 #document.onmousedown event-listener nrWrapper …com/browser-performance/newrelic.js 10 1 #document.onpointerdown event-listener nrWrapper …com/browser-performance/newrelic.js 28 2 DOMWindow.onclick event-listener nrWrapper …com/browser-performance/newrelic.js 136 1 A.onclick event-listener nrWrapper …com/browser-performance/newrelic.js 10 2 BODY.onclick event-listener nrWrapper …com/browser-performance/newrelic.js 26 2 #document.ontouchstart event-listener nrWrapper …com/browser-performance/newrelic.js 13 1 #document.onclick event-listener nrWrapper …com/browser-performance/newrelic.js 14 2 DIV#root.onclick event-listener nrWrapper …com/browser-performance/newrelic.js 167 1

Attribution Issues: Versioned scripts Versioned / hashed resources are common in modern build processes …/_next/static/chunks/1dd3208c-191771bcb3be71a1.js …/_next/static/chunks/914-ff512dbd6d857625.js …/_next/static/chunks/937-c796c646a443351b.js They can make it hard to track resource performance over time @andydavies.me

(program: 2) @andydavies.me

Name sources using //# sourceURL <script type=”text/javascript”> document.body.addEventListener(“click”, function(){ for(var a=Date.now()+2E3;Date.now()<a;); }); //# sourceURL=Use-the-sourceURL-Luke </script> Will be used for sourceURL, works with external scripts too Example: https://andydavies.github.io/agent-tests/loaf/sourceurl.html @andydavies.me

No Script Entries for • Extensions For privacy reasons we can’t know what extensions a visitor is using Chrome on Android doesn’t support extensions • Garbage Collection Tickets open for opaque attribution of these @andydavies.me

https://webperf-snippets.nucliweb.net/Interaction/Long-Animation-Frames-Helpers

https://github.com/andydavies/perf-timeline-to-devtools-profile

I ♥ LoAF

LoAF provides insights into the runtime costs of the code we ship And allow us to identify our problem scripts Within the context of our visitors environment @andydavies.me

You’re going to need RUM Photo by Ash Edmonds on Unsplash

Feel like I’ve just scratched the surface… … and there’s more for LoAF to reveal @andydavies.me Photo by Mat Ranson on Unsplash

Finally… if you’re flying out of Schipol… Photo by Erwin Hofman

Thanks! @andydavies.me andy.davies@speedcurve.com