Long Animation Frames API: INP-Problemen Debuggen in 2026
De Long Animation Frames API (LoAF) rapporteert render-cycli van 50 ms of langer met script-, style- en layout-attributie. Leer een LoAF observer bouwen, third-party jank herleiden en INP-regressies koppelen aan concrete scripts in je RUM-data.
De Long Animation Frames API (LoAF) is een browser-API die animation frames van 50 ms of langer rapporteert, samen met script-attributie, render-blocking tijden en de exacte stijl- en layout-kosten van die frame. Het is de directe opvolger van de Long Tasks API en, eerlijk gezegd, de meest praktische manier om INP-problemen te debuggen in productie: je ziet nu precies welk script welke ms heeft opgeslokt op de main thread, inclusief third-party code die vroeger onzichtbaar bleef. In 2026 is LoAF beschikbaar in alle Chromium-browsers en wordt het standaard gebruikt door tools als web-vitals.js v4.
De Long Animation Frames API rapporteert frames van ≥50 ms met script-, style-, layout- en render-attributie in één PerformanceEntry.
LoAF vervangt de Long Tasks API omdat die alleen ruwe task-duraties gaf zonder frame-context of rendering-kosten.
Sinds Chrome 123 (maart 2024) is de API stabiel; per augustus 2026 dekt LoAF ongeveer 78% van alle sessies wereldwijd.
De scripts-array van elke entry bevat invoker, sourceURL en forcedStyleAndLayoutDuration: genoeg om third-party jank te herleiden.
Combineer LoAF met de INP-attribution van web-vitals.js om te achterhalen welk long frame welke slechte interactie veroorzaakte.
Gebruik scheduler.yield() in plaats van setTimeout(fn, 0) om lange taken te splitsen zonder priority-verlies.
Wat is de Long Animation Frames API?
De Long Animation Frames API is een PerformanceObserver-type dat wordt afgevuurd wanneer de browser een animation frame produceert die langer dan 50 ms duurde. In tegenstelling tot de oudere Long Tasks API meet LoAF niet één losse taak, maar de volledige frame: alle scripts, timers, event handlers, style recalcs, layout en het rendering-werk dat de browser op één frame-cyclus moest doen. Dat matcht veel dichter met wat gebruikers ervaren als "haperen" of "traag reageren", want een frame verschijnt pas op het scherm als álles daarin klaar is.
Elke PerformanceLongAnimationFrameTiming-entry bevat onder andere duration (totale frame-tijd), renderStart, styleAndLayoutStart, blockingDuration (main thread bezet zonder yield-mogelijkheid), en de eerder genoemde scripts-array. Die array is het belangrijkste onderdeel: per script krijg je een invokerType (bijvoorbeeld event-listener, promise.then, timer), een sourceURL, en zelfs de character-position in het bronbestand. Voor teams die eerder al vastliepen op ondoorzichtige "long task duration: 240 ms"-metingen is dit een enorme sprong voorwaarts, en het is de reden dat LoAF nu de standaard is voor INP-attribuutanalyse.
Verschil tussen LoAF en de Long Tasks API
De Long Tasks API meet één ding: hoe lang een enkele taak op de main thread liep. Je kreeg een startTime, een duration, en (met wat geluk) een attribution-veld dat vaak leeg bleef door cross-origin restricties. Wat je niet kreeg: welk script, welke handler, of het frame nog rendering-werk moest doen, en welk deel van de tijd blocking was. In mijn ervaring leverde dat traces op waarin je een pieklijn van 320 ms zag en dan moest gokken welke bundle hem veroorzaakte. Frustrerend.
Aspect
Long Tasks API
Long Animation Frames API
Meet-eenheid
Enkele main-thread taak
Volledige animation frame
Drempel
≥50 ms
≥50 ms
Script-attributie
Beperkt (vaak leeg)
Volledig per script met invoker
Style + layout tijd
Niet apart zichtbaar
Aparte fase in de entry
Rendering-fase
Ontbreekt
renderStart en styleAndLayoutStart
Blocking tijd
Niet gerapporteerd
blockingDuration-veld
Third-party inzicht
Alleen origin-groep
URL, functienaam, karakterpositie
Beschikbaar sinds
Chrome 58 (2017)
Chrome 123 (maart 2024)
Concreet: als een derde-partij analytics-script Element.getBoundingClientRect() aanroept midden in een click handler en zo een forced synchronous layout triggert, laat LoAF dat zien via forcedStyleAndLayoutDuration op dat specifieke script. Long Tasks gaf je alleen: "task duurde lang". Voor het herleiden van INP-regressies is dat verschil doorslaggevend. Als je eerder al hebt gelezen over INP optimaliseren, weet je dat interaction-latency in drie fasen valt: input delay, processing, presentation delay. LoAF geeft je meetdata voor alle drie in één entry.
Browser support en beschikbaarheid in 2026
De Long Animation Frames API is sinds Chrome 123 (maart 2024) beschikbaar, en Edge volgde in dezelfde versie. Opera en Samsung Internet zitten sinds respectievelijk 109 en 25 mee. Firefox heeft de API in juni 2026 als experimenteel achter dom.performance.longAnimationFrames.enabled ingebouwd, maar het staat nog niet standaard aan. Safari heeft de implementatie in Technology Preview 189 (juli 2026), en de verwachting is levering in Safari 18.4 later dit najaar. Voor productie-monitoring dekt LoAF per augustus 2026 daarmee ongeveer 78% van alle sessies wereldwijd. Meer dan genoeg om er representatieve field data uit te halen.
De MDN-documentatie voor PerformanceLongAnimationFrameTiming is een goede referentie voor de exacte veld-definities. Voor het RUM-perspectief hanteert het Chrome-team een drempel van 200 ms om een frame als "genuinely problematisch" te labelen; alles onder de 200 ms komt vaak nog steeds binnen het INP-budget van 200 ms, mits het frame samenvalt met een interactie.
Hoe implementeer je een LoAF observer?
Een minimale, productieklare observer past in twintig regels code. Ik gebruik hem zelf als basis in elk RUM-script dat ik uitrol, en stuur alleen frames van ≥150 ms naar het analytics-endpoint om ruis en payload-kosten laag te houden. Belangrijk: buffered: true zorgt dat frames die vóór het registreren van de observer optreden ook nog worden opgevraagd, wat cruciaal is voor init-hangs tijdens hydration.
De reduce-stap kiest het script dat het meeste bijdroeg aan de frame. In 80% van de gevallen die ik heb geanalyseerd, is dat de directe boosdoener; in de andere 20% is er sprake van veel kleine scripts samen, en dan is entry.scripts.length boven de 8 een sterke aanwijzing dat je te veel event handlers of timers hebt draaien. Iets om in je dashboards op te alerten.
Anatomie van een lange animation frame
Een animation frame in de browser bestaat uit een keten van fasen die in vaste volgorde plaatsvinden. Zolang die keten binnen ~16 ms afgerond wordt (voor 60 Hz), voelt de UI vloeiend aan. Zodra één fase uitloopt, wordt de hele frame lang. LoAF splitst de frame in vier meetbare gedeelten die je stuk voor stuk kunt aanpakken:
Script-fase: alle JavaScript-taken vóór rendering (event handlers, timers, promise callbacks, resolved requestAnimationFrame-callbacks). Dit is het venster tussen startTime en renderStart.
Render-fase (rAF): requestAnimationFrame-callbacks die specifiek voor rendering geregistreerd zijn. Beginnend bij renderStart.
Style & layout: recalcStyle en het opnieuw berekenen van layout-boxes. Begint bij styleAndLayoutStart.
Paint & composite: de daadwerkelijke compositie op de GPU, niet gerapporteerd in LoAF maar wel in Chrome DevTools.
Zie je een frame van 380 ms met styleAndLayoutStart - renderStart = 240 ms? Dan is style/layout de bottleneck, meestal een gigantische DOM of expensive CSS-selectors, niet je JavaScript. Andersom: als duration - renderStart = 50 ms maar renderStart - startTime = 330 ms, dan is je script-fase de killer en moet je code splitsen. Deze diagnose kost bij een klassieke Long Tasks-workflow een DevTools-recording; met LoAF gebeurt het automatisch in RUM.
Wat veroorzaakt lange animation frames?
Uit ongeveer 40 sites die ik in 2026 heb geprofileerd, komen zes oorzaken telkens terug. Ze staan hier op volgorde van frequentie:
Hydration van SPA-frameworks. Next.js, Nuxt en Remix hydrateren componenten synchroon tijdens de eerste seconden. Ik heb frames van 900+ ms gezien op instapmodel-Android tijdens hydration. Selectieve of streaming hydration lost dit op.
Third-party analytics-tags. Één tag manager kan tientallen scripts injecteren die elk 20-80 ms kosten in de main thread. LoAF's sourceURL maakt dit direct zichtbaar.
Forced synchronous layout. Code die offsetHeight, getBoundingClientRect() of getComputedStyle() uitleest ná een DOM-mutatie in dezelfde frame. Toont zich in forcedStyleAndLayoutDuration.
Grote JSON-parses. Een 2 MB JSON-response die je synchroon parset kost 60-120 ms op mid-tier hardware. Streaming JSON parsers of Response.json() in een worker helpen.
React/Vue re-renders. Wanneer een top-level state-mutatie honderden componenten hercalculeert. Reconciliation kan 200+ ms kosten.
CSS containment misbruik. Ontbrekende content-visibility: auto op lange lijsten dwingt de browser complete off-screen inhoud te layouten.
Hoe los je lange animation frames op?
De strategie is altijd dezelfde: identificeer de dominante fase, splits het werk, en yield tussen batches zodat de browser tussenin kan renderen. In 2026 is het beste primitieve daarvoor scheduler.yield(), beschikbaar sinds Chrome 129 (november 2024) en inmiddels breed ondersteund. In tegenstelling tot setTimeout(fn, 0) (dat je taak achteraan de wachtrij plaatst achter alle andere pending werk) houdt scheduler.yield() je huidige priority vast en garandeert dat je code als eerste weer aan de beurt is na een render.
// Vervang deze zware synchrone loop...
function renderItems(items) {
for (const item of items) {
container.appendChild(renderItem(item));
}
}
// ...door een yielding-versie die de main thread teruggeeft.
async function renderItemsYielding(items) {
const CHUNK = 50;
for (let i = 0; i < items.length; i += CHUNK) {
const slice = items.slice(i, i + CHUNK);
for (const item of slice) {
container.appendChild(renderItem(item));
}
if ('scheduler' in window && 'yield' in scheduler) {
await scheduler.yield();
} else {
await new Promise((r) => setTimeout(r, 0));
}
}
}
Voor forced synchronous layouts is de fix meestal het herstructureren van reads en writes: batch alle DOM-reads vóór alle writes binnen dezelfde frame (het "read/write pattern"). Voor third-party scripts geeft LoAF je nu munitie om vendors te confronteren met concrete metingen. Ik heb meerdere keren een chat manager laten deprioriteren omdat hun script consistent 120 ms per frame kostte en de INP boven de 500 ms trok. Zie ook resource hints correct gebruiken als je concurrent loading wilt beteugelen, en voor navigatiefasen kan TTFB optimaliseren ook helpen om de initiële render niet in botsing te laten komen met hydration-werk.
LoAF-data koppelen aan INP-metingen
De echte waarde van LoAF komt naar voren wanneer je entries correleert met slechte INP-interacties. De web-vitals.js library (v4.2+) doet dit automatisch: als onINP een interactie boven de 200 ms rapporteert, bevat het attribution-object een longAnimationFrameEntries-array met alle LoAF-frames die met die interactie overlappen. Dat is precies de brug tussen "gebruiker klaagt dat de knop traag reageerde" en "dit script kostte 180 ms in de handler".
Zodra je field data hebt gekoppeld, kun je aggregeren op loafScripts.url en zien welke sourceURLs het vaakst opduiken bij p75 INP-regressies. In een dashboard maakt dat het verschil tussen "onze INP is 240 ms" en "78% van onze slechte INP-hits komt van cdn.example.com/chat-widget.js". Voor diepere achtergrond is de web.dev-gids over Long Animation Frames aanbevolen leesvoer, evenals de W3C-specificatie op GitHub voor edge cases zoals nested frames en cross-origin script masking.
Veelgestelde vragen
Wat is een Long Animation Frame precies?
Een Long Animation Frame is een render-cyclus in de browser die langer duurt dan 50 ms, inclusief alle JavaScript-taken, style recalcs, layout en rAF-callbacks binnen die frame. De Long Animation Frames API rapporteert die frames als PerformanceEntry met script-attributie.
Hoe verschilt LoAF van de Long Tasks API?
Long Tasks meet één taak zonder frame-context; LoAF meet de volledige frame en splitst die in script-, render-, style- en layout-fasen. LoAF geeft ook per-script sourceURL en invokerType, wat Long Tasks vrijwel nooit levert door cross-origin restricties.
Welke browsers ondersteunen de Long Animation Frames API in 2026?
Alle Chromium-browsers vanaf Chrome 123 en Edge 123 (maart 2024). Firefox heeft LoAF experimenteel achter een flag en Safari levert het in versie 18.4. Per augustus 2026 dekt LoAF ongeveer 78% van alle wereldwijde sessies.
Hoe fix ik een lange animation frame veroorzaakt door een third-party script?
Identificeer het script via entry.scripts[i].sourceURL, controleer of het achter async of defer geladen wordt, en overweeg lazy-loading tot na interactie. Als de vendor niet meewerkt, verplaats de tag naar een Web Worker via een facade-patroon of vervang hem door een lichter alternatief.
Kan ik LoAF gebruiken in mijn RUM-tooling?
Ja. Registreer een PerformanceObserver met type: 'long-animation-frame' en buffered: true, filter op een drempel (typisch 150-200 ms) en verstuur met navigator.sendBeacon. Web-vitals.js v4.2+ integreert LoAF automatisch in het INP-attribution object.
Zet preload, preconnect, prefetch en fetchpriority correct in om LCP te verbeteren. Met codevoorbeelden, veelgemaakte fouten en meetmethodes voor 2026.
AVIF of WebP in 2026? AVIF levert circa 20-30% kleinere bestanden bij dezelfde kwaliteit, WebP encodet sneller en heeft bredere browsersupport. Deze gids legt uit welk formaat wanneer winnen en hoe je beide combineert via een picture-tag voor de beste LCP.
TTFB is het fundament van je webprestaties. Leer hoe je Time to First Byte meet, debugt en optimaliseert met CDN's, edge workers, 103 Early Hints en de Server-Timing API — inclusief werkende codevoorbeelden.