Long Animation Frames API 2026: Så hittar du INP-flaskhalsar med fältdata
Long Animation Frames API rapporterar varje renderingsram >50 ms med skript-, käll- och tidsattribution. Så samlar du datan i fält och kopplar den till INP-händelser i produktion.
Long Animation Frames API (LoAF) är ett webb-API som rapporterar varje renderingsram som tar längre än 50 ms, och berättar exakt vilket skript, vilken hanterare och vilket arbete som blockerade huvudtråden. För dig som jagar dåliga Interaction to Next Paint-siffror i fält är det här den första riktiga attributionssignalen vi någonsin fått. Jag har kört LoAF i produktion sedan det blev baseline i Chrome 123 (mars 2024), och i den här guiden visar jag hur du samlar in datan via RUM, kopplar den till INP-händelser och förvandlar p75-rapporter till konkreta åtgärder.
LoAF rapporterar alla ramar >50 ms och är efterföljaren till det grovkorniga Long Tasks API, med skript-, käll- och tidsattribution per fas.
API:et är baseline i alla Chromium-webbläsare sedan Chrome 123 (mars 2024). Safari och Firefox saknar fortfarande stöd i juni 2026.
En lång animationsram består av fem mätbara faser: startTime, renderStart, styleAndLayoutStart, duration och blockingDuration.
Egenskapen scripts[] ger dig invoker, sourceURL och sourceCharPosition, så du ser exakt vilken funktion som blockerade ramen.
Kombinera PerformanceObserver för long-animation-frame med event-entries för att attribuera dåliga INP-värden till specifika skript i produktion.
LoAF ersätter inte syntetiska Lighthouse-tester. Det kompletterar dem med fältdata som speglar riktiga användares enheter och nätverk.
Vad är Long Animation Frames API?
Long Animation Frames API är ett webb-API som exponerar PerformanceLongAnimationFrameTiming-entries varje gång webbläsaren producerar en renderingsram som tar mer än 50 ms från start till presentation. Tröskeln på 50 ms är inte godtycklig. Den motsvarar tre missade ramar vid 60 Hz och korrelerar starkt med den övre gränsen för "god" INP enligt Core Web Vitals. När en ram överskrider gränsen rapporterar API:et inte bara att den var långsam, utan också varför: vilka skript som kördes, varifrån de laddades, hur mycket tid som spenderades på style och layout, och om ramen tvingade fram en synkron layoutomberäkning.
För mig som jobbar med fältdata varje dag är det viktigaste att LoAF är en per-ram-signal, inte en per-task-signal. En enda långsam INP-interaktion består ofta av flera tasks som tillsammans blockerar en ram. Long Tasks API missade den helheten. Det rapporterade enskilda tasks >50 ms men sa ingenting om ramens deadline eller styling/layout-arbetet efteråt. Läs MDN:s referens för PerformanceLongAnimationFrameTiming för den fullständiga gränssnittsspecifikationen.
Hur skiljer sig LoAF från Long Tasks API?
Den korta versionen: Long Tasks API berättar att "något var långsamt", LoAF berättar "vad, var och hur länge". Den långa versionen kräver en tabell, eftersom skillnaderna direkt påverkar vilket API du ska instrumentera i din RUM.
Egenskap
Long Tasks API
Long Animation Frames API
Granularitet
Enskild task >50 ms
Hel renderingsram >50 ms
Skriptattribution
Endast container (window/iframe)
Funktionsnamn, käll-URL, teckenposition
Style/layout-tid
Inte rapporterad
Egen fas via styleAndLayoutStart
Forced reflow-upptäckt
Inte möjlig
Implicit via skriptens forcedStyleAndLayoutDuration
Korrelation med INP
Svag (saknar renderingsfas)
Stark, täcker hela ramens livscykel
Webbläsarstöd 2026
Chromium + delvis Safari
Endast Chromium (Chrome 123+)
Status
Underhållsläge, ersätts gradvis
Aktivt utvecklat, baseline sedan 2024
I praktiken kör jag fortfarande båda API:erna parallellt i min RUM under en övergångsperiod, eftersom longtask-entries är det enda som fungerar i Safari. Men för Chromium-trafik (vilket är 70–80 % av användarna på de flesta sajter jag mäter) är LoAF överlägset. Vi täckte huvudtrådsblockering ur ett annat perspektiv i min guide till JavaScript bundle-optimering. LoAF är det diagnostiska verktyget som visar exakt vilka bundle-delar som är värst i produktion.
Anatomin av en lång animationsram
En PerformanceLongAnimationFrameTiming-entry är inte ett enda tidsstämpel. Det är en uppsättning faser som tillsammans rekonstruerar ramens hela livscykel. Att förstå dessa faser är skillnaden mellan att gissa och att veta var flaskhalsen sitter.
startTime: När ramen började bearbeta input eller schemalagda tasks.
renderStart: När webbläsaren började rendera (begagnade requestAnimationFrame-callbacks körs här).
styleAndLayoutStart: När style- och layoutberäkningar började. Perioden mellan renderStart och denna gräns är ren skript-tid efter rAF.
duration: Total tid från startTime till presentation.
blockingDuration: Den del av ramen som blockerade ny input. Det här är vad som faktiskt skadar INP.
firstUIEventTimestamp: Tidpunkten för den första inputhändelsen som väntade på den här ramen.
Så samlar du in LoAF-data med PerformanceObserver
Det här är minsta möjliga produktionsklara LoAF-instrumentering. Den buffrar entries som inträffade innan din kod laddades, så att du inte missar långa ramar under sidans initiala laddning.
// loaf-observer.js — körs så tidigt som möjligt i <head>
if (typeof PerformanceObserver !== 'undefined' &&
PerformanceObserver.supportedEntryTypes &&
PerformanceObserver.supportedEntryTypes.includes('long-animation-frame')) {
const loafEntries = [];
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Filtrera bort ramar under 100 ms för att begränsa datavolym
if (entry.duration < 100) continue;
loafEntries.push({
startTime: Math.round(entry.startTime),
duration: Math.round(entry.duration),
blockingDuration: Math.round(entry.blockingDuration),
renderStart: Math.round(entry.renderStart),
styleAndLayoutStart: Math.round(entry.styleAndLayoutStart),
scripts: entry.scripts.map((s) => ({
name: s.name,
invoker: s.invoker,
invokerType: s.invokerType,
sourceURL: s.sourceURL,
sourceCharPosition: s.sourceCharPosition,
duration: Math.round(s.duration),
forcedStyleAndLayoutDuration: Math.round(s.forcedStyleAndLayoutDuration)
}))
});
}
});
// buffered: true plockar upp ramar som inträffade före observer-skapandet
observer.observe({ type: 'long-animation-frame', buffered: true });
// Exponera för din RUM-rapportör
window.__loafEntries = loafEntries;
}
Tre detaljer som ofta missas. Först måste du sätta buffered: true, annars förlorar du de viktigaste ramarna (de som inträffar under sidladdning innan din kod ens körts). För det andra: lagra entries i en buffer, skicka dem inte direkt vid varje callback (kostsamt, och hamnar i samma huvudtråd du försöker mäta). För det tredje: filtrera tröskelvärdet på klientsidan. Att rapportera varje 51 ms-ram fyller din analyspipeline med brus.
Skicka LoAF-data till din RUM-pipeline
Datan är värdelös om den stannar i webbläsaren. Här är mönstret jag använder för att skicka LoAF-entries till en analysendpoint utan att själv skapa nya långa ramar.
// loaf-reporter.js
function reportLoafBatch() {
if (!window.__loafEntries || window.__loafEntries.length === 0) return;
const payload = JSON.stringify({
url: location.pathname,
deviceMemory: navigator.deviceMemory || null,
hardwareConcurrency: navigator.hardwareConcurrency || null,
connection: navigator.connection ? {
effectiveType: navigator.connection.effectiveType,
rtt: navigator.connection.rtt,
downlink: navigator.connection.downlink
} : null,
entries: window.__loafEntries.splice(0) // töm bufferten
});
// sendBeacon överlever pagehide och blockerar inte huvudtråden
if (navigator.sendBeacon) {
navigator.sendBeacon('/rum/loaf', payload);
} else {
fetch('/rum/loaf', { method: 'POST', body: payload, keepalive: true });
}
}
// Rapportera vid visibilitychange (täcker både bakgrundsflik och stängning)
addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') reportLoafBatch();
});
// Backup vid pagehide för Safari/iOS
addEventListener('pagehide', reportLoafBatch);
Berika alltid payloaden med enhetskontext: deviceMemory, hardwareConcurrency och Network Information API. Jag har sett 95-percentilen för INP variera med en faktor på 8 mellan low-end Android (2 GB RAM) och high-end desktop på samma sajt. Utan enhetsklass i dina percentiler är ditt RUM-genomsnitt en bekväm lögn.
Koppla LoAF-ramar till INP-händelser
Och så kommer vi till den verkliga superkraften: korrelation mellan en INP-händelse och LoAF-ramen som producerade den. Du kan göra det genom att jämföra tidsstämplar. När du fångar en dålig INP-händelse, hitta den LoAF-entry vars startTime och duration överlappar interaktionens tidsfönster.
import { onINP } from 'web-vitals/attribution';
onINP((metric) => {
if (metric.value < 200) return; // Endast "behöver förbättras" och sämre
const interactionStart = metric.attribution.interactionTime;
const interactionEnd = interactionStart + metric.value;
// Hitta alla LoAF-ramar som överlappar interaktionen
const matchingFrames = (window.__loafEntries || []).filter((f) => {
const frameEnd = f.startTime + f.duration;
return f.startTime <= interactionEnd && frameEnd >= interactionStart;
});
// Aggregera vilka skript som dominerade
const scriptTotals = new Map();
for (const frame of matchingFrames) {
for (const script of frame.scripts) {
const key = `${script.sourceURL}:${script.invoker}`;
scriptTotals.set(key, (scriptTotals.get(key) || 0) + script.duration);
}
}
navigator.sendBeacon('/rum/inp-attribution', JSON.stringify({
inp: metric.value,
target: metric.attribution.interactionTarget,
eventType: metric.attribution.interactionType,
inputDelay: metric.attribution.inputDelay,
processingDuration: metric.attribution.processingDuration,
presentationDelay: metric.attribution.presentationDelay,
loafFrames: matchingFrames.length,
topScripts: [...scriptTotals.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
}));
});
Det här mönstret är vad web.dev:s officiella LoAF-guide rekommenderar, och det är så Chrome DevTools internt kopplar interaktioner till skript i Performance-panelen. För djupare bakgrund kring INP-attributering har jag skrivit en separat komplett guide till INP-optimering 2026 som täcker själva metriken.
Konkreta åtgärder baserat på LoAF-attribution
Datan är inte ett mål i sig. Den ska leda till åtgärder. Här är beslutsträdet jag använder när jag granskar en LoAF-rapport.
Om scripts[].forcedStyleAndLayoutDuration dominerar
Du har layout thrashing. Sök efter den sourceURL och sourceCharPosition som rapporteras. Det är ofta en loop som läser offsetHeight efter att ha skrivit till style. Lösningen är att batchera DOM-läsningar före skrivningar med requestAnimationFrame.
Om en enskild skript-entry är >100 ms
Bryt upp arbetet med scheduler.yield() (baseline i Chromium sedan 2024) eller scheduler.postTask med lämplig prioritet. För äldre webbläsare, fall tillbaka till setTimeout(fn, 0) mellan logiska enheter.
Om invokerType är event-listener och invoker pekar på ett tredjepartsskript
Du har en tagghanterare eller ett analytics-bibliotek som blockerar interaktioner. Antingen ladda det med type="module" och defer, flytta till en web worker via Partytown, eller (i värsta fall) eskalera till leverantören med konkret bevis. Jag har vunnit den argumentationen två gånger det senaste året enbart tack vare LoAF-loggar.
Om renderStart är sent men inga skript dominerar
Du har CSS- eller layoutkostnad utan motsvarande JS-orsak. Granska komplexa selektorer, breda :has()-användningar och containerfrågor som tvingar omvärderingar. CSS Containment kan hjälpa här.
Vilka webbläsare stöder Long Animation Frames?
I juni 2026 ser stödet ut så här: alla Chromium-baserade webbläsare (Chrome, Edge, Opera, Brave, Samsung Internet) stöder LoAF sedan Chrome 123 (mars 2024). Safari implementerar inte API:et och har inte signalerat någon avsikt att göra det. Firefox har en aktiv buggrapport men inget releasedatum. Du kan följa den aktuella statusen via Chrome Status-sidan för Long Animation Frames.
Praktiskt betyder det att din LoAF-instrumentering kommer att täcka 70–80 % av användarna på en genomsnittlig sajt, ungefär samma täckning som Chrome User Experience Report (CrUX). För Safari-användare behöver du en separat strategi: kombinera longtask-entries med Event Timing API och INP-attribution. Det ger inte samma djup, men det är allt vi har tills WebKit rör sig.
Vanliga fallgropar och tolkningsfel
Tre misstag jag ser om och om igen när team börjar med LoAF.
Att rapportera medelvärden i stället för p75/p95. Långa ramar har en kraftig long-tail-fördelning. Medelvärdet kommer alltid att se acceptabelt ut medan p95 brinner. Använd histogram, inte medelvärden.
Att glömma att sourceURL kan vara tom. Inline-skript och eval har ingen URL. Hantera det fallet i din aggregator, annars förlorar du attribution för en stor del av dina ramar.
Att tro att duration är det enda värdet som spelar roll. En 500 ms-ram med 50 ms blockerande tid är inte ett INP-problem. Det är möjligen ett rendering-pipeline-problem. Mät blockingDuration separat.
Ärligt talat: det här är den sortens API jag önskar att vi haft 2019. För mer kontext kring varför fältdata slår syntetiska tester på just den här typen av problem rekommenderar jag Chrome-teamets djupgenomgång av Long Animation Frames. Den förklarar varför Lighthouse Total Blocking Time aldrig kommer att fånga det LoAF fångar i fält.
Vanliga frågor
Vad är Long Animation Frames API på enkel svenska?
Det är ett webbläsar-API som rapporterar varje renderingsram som tar längre än 50 ms och berättar exakt vilket JavaScript, vilken stilberäkning och vilken layout som blockerade ramen. Du använder det i din RUM-pipeline för att hitta varför riktiga användare upplever långsamma interaktioner.
Är Long Animation Frames API samma sak som Long Tasks API?
Nej. Long Tasks API rapporterar enskilda tasks >50 ms utan skriptattribution. LoAF rapporterar hela renderingsramen, inklusive style, layout och tvingad reflow, och ger dig sourceURL, funktionsnamn och teckenposition för varje skript som körde under ramen.
Kan jag använda LoAF för att förbättra mitt INP-värde?
Ja, och det är just det API:et designades för. När du korrelerar LoAF-entries med INP-händelser via tidsstämplar kan du identifiera vilka skript som producerade de blockerande ramarna under en interaktion och åtgärda dem riktat i stället för att gissa.
Fungerar Long Animation Frames API i Safari?
Nej, inte i juni 2026. Endast Chromium-baserade webbläsare (Chrome 123+, Edge, Opera, Samsung Internet, Brave) stöder API:et. För Safari-trafik behöver du falla tillbaka till Event Timing och INP-attribution från web-vitals-biblioteket.
Hur mycket data genererar LoAF i produktion?
På en typisk sida ser jag 5–30 LoAF-entries per session beroende på interaktivitetsnivå. Med klientfiltrering vid 100 ms och batchad rapportering via sendBeacon hamnar payloaden under 5 KB per session, försumbart jämfört med en genomsnittlig analytics-tagg.
Praktisk guide till Speculation Rules API i Chrome 2026: så konfigurerar du prerender och prefetch för omedelbar navigation utan att förstöra analytics eller bränna mobil bandbredd.
Praktisk guide till bildoptimering 2026: välj rätt format (AVIF/WebP), använd srcset och sizes korrekt, och prioritera LCP-bilden med fetchpriority. Innehåller färdiga kodexempel.
Lär dig minska din JavaScript-bundlestorlek med code splitting, tree shaking, minifiering och prestandabudgetar. Praktisk guide med kodexempel och verktyg för snabbare webbladdning 2026.