Long Animation Frames API 2026: az INP problémák felderítése LoAF-fal
A Long Animation Frames API pontosan megmondja, melyik szkript, layout vagy stílusszámítás okozza az INP lassulást. Működő kód, RUM integráció és böngészőtámogatás 2026-ban.
A Long Animation Frames API (röviden LoAF) egy 2024-ben véglegesített böngészős mérőeszköz, amely megmutatja, pontosan melyik szkript, layout vagy stílusszámítás okozta, hogy egy renderelési frame 50 ms-nál tovább tartott. Így közvetlenül azonosítható az INP-lassulások gyökere. A korábbi Long Task API csak azt mondta meg, hogy „valami" beragadt; a LoAF viszont megnevezi a függvényt, a forgatókönyvet, a script URL-t, sőt még azt is, hogy melyik iframe futtatta. Ha az INP-metrika 200 ms fölé csúszik és nincs sejtésed, hol veszted az időt a fő szálon, ez az API az, amit keresel.
A Long Animation Frames API-t a Chrome 123 óta lehet éles környezetben használni, és 2026-ban már a Chromium-alapú böngészők (Chrome, Edge, Opera, Brave) mindegyike támogatja.
Egy „long animation frame" olyan renderelési ciklus, amely legalább 50 ms-ig tart. A specifikáció szerint ez az a küszöb, ahonnan a felhasználó észreveszi az akadozást.
A LoAF a Long Task API-hoz képest attribúciót ad: script neve, invoker típus, karakterpozíció, forrás iframe és a blokkoló időtartam külön van bontva.
A PerformanceObserver API-n keresztül kell feliratkozni, és a renderStart, styleAndLayoutStart, valamint a scripts tömb adja a diagnózishoz szükséges részleteket.
A web-vitals.js 4.x már natívan használja a LoAF-ot az onINP attribution módban, hogy a leglassabb interakcióhoz csatolja a felelős szkriptet.
A fallback stratégia egyszerű: ha a PerformanceObserver.supportedEntryTypes nem tartalmazza a long-animation-frame értéket, essünk vissza a Long Task API-ra.
Mi az a Long Animation Frames API?
A Long Animation Frames API egy alacsony költségű megfigyelő interfész, amely minden olyan renderelési frame-ről bejegyzést készít, aminek teljes időtartama meghaladja a 50 ms-ot. A specifikáció szerint egy „animation frame" a böngészőnek attól a pillanattól tart, amikor elkezdi a rendering ciklus előkészítését, egészen addig, míg a képernyőre került az új képkocka. Ha ez az intervallum hosszabb 50 ms-nál, az entry a PerformanceObserver-be kerül. A 50 ms-os küszöb nem véletlen: a Response Time Guidelines szerint ez az a határ, ahonnan a felhasználó „nem azonnalinak" érzékeli a reakciót, és ez pontosan az a tartomány, ahol az INP metrika elkezd 200 ms fölé csúszni.
A régi Long Task API csak egyetlen dimenziót adott: „valami 50 ms-nál tovább tartott a főszálon". A LoAF ezzel szemben minden entry-hez visszaadja a renderStart, styleAndLayoutStart, firstUIEventTimestamp, blockingDuration és a scripts tömb mezőit. Ebből kiolvasható, hogy a frame melyik része volt a szkriptvégrehajtás, melyik a stílusszámítás és a layout, melyik pedig a paint. Ez az én workflow-mban azt jelenti, hogy nem kell többet console.time-mal bombázni a kódbázist annak érdekében, hogy megtudjam, melyik listener okoz akadást. A böngésző megmondja helyettem.
Long Animation Frames vs. Long Task API
A két API-t gyakran összekeverik, mert mindkettő ugyanazt az 50 ms-os küszöböt használja. A különbség azonban jelentős, és 2026-ban gyakorlatilag már nincs okod a Long Task API-t használni új projektben. Az alábbi táblázat összegzi a fő eltéréseket, amiket egy tényleges profilozási session során tapasztaltam:
Jellemző
Long Task API
Long Animation Frames API
Küszöb
50 ms egy task-ra
50 ms egy teljes frame-re
Attribúció
Csak a container (iframe) neve
Script URL, függvény, karakterpozíció, invoker típus
A gyakorlatban ez a különbség a következőt jelenti. A Long Task API-val tudom, hogy egy 320 ms-os task megakasztotta a felhasználót; a LoAF-fal viszont azt is, hogy ez a 320 ms 240 ms scriptvégrehajtás plusz 60 ms layout plusz 20 ms paint volt, és a script a https://cdn.example.com/analytics-v3.js 8412. karakterénél kezdődő függvény. Ez órákat spórol meg egy debugging session során. Őszintén szólva ezért is dobtam ki a régi Long Task hook-jaimat egy nagyobb SPA-nál még tavaly.
Hogyan használjuk a LoAF-ot a gyakorlatban?
A legegyszerűbb integrációhoz egy PerformanceObserver-t regisztrálunk a long-animation-frame típusra, majd feldolgozzuk az entry-ket. Ez a snippet lokálisan is elfut, és a konzolban részletes attribúciót ír ki minden 50 ms fölötti frame-hez:
// LoAF observer teljes attribúcióval
const loafObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.group(`LoAF frame: ${entry.duration.toFixed(1)} ms`);
console.log('Blocking:', entry.blockingDuration.toFixed(1), 'ms');
console.log('Render start delay:',
(entry.renderStart - entry.startTime).toFixed(1), 'ms');
console.log('Style + layout:',
(entry.styleAndLayoutStart
? entry.duration - (entry.styleAndLayoutStart - entry.startTime)
: 0).toFixed(1), 'ms');
// A script attribúció itt kezdődik
for (const script of entry.scripts) {
console.log(
`[${script.invokerType}]`,
script.invoker,
'→',
script.sourceURL,
`${script.sourceCharPosition}`,
`(${script.duration.toFixed(1)} ms)`
);
}
console.groupEnd();
}
});
loafObserver.observe({
type: 'long-animation-frame',
buffered: true // már meglévő entry-k is
});
A buffered: true opció azt biztosítja, hogy az observer regisztrálása előtt keletkezett entry-ket is megkapod. Ez különösen hasznos, ha a mérőkódot lazy módon töltöd. Az invokerType mező hét lehetséges értéket vehet fel: user-callback, event-listener, resolve-promise, reject-promise, classic-script, module-script, console-log. Az én tapasztalatom szerint a event-listener és a resolve-promise a leggyakoribb bűnös produkciós site-okon (általában egy vastag click handler vagy egy async függvény, ami tömbök százain iterál).
Harmadik fél szkriptek felderítése LoAF-fal
A produkciós teljesítményproblémák jelentős része harmadik fél szkriptekből ered: analytics, tag manager, chat widget, A/B teszt SDK. A régi Long Task API csak azt mondta meg, hogy egy iframe csinálja, de a modern világban ezek a szkriptek gyakran közvetlenül a fő dokumentumban futnak. A LoAF sourceURL mezője viszont pontos attribúciót ad. Ez különösen jól illeszkedik a harmadik fél szkriptek optimalizálási stratégiájához, mert számokkal igazolható, hogy egy adott vendor SDK valóban rontja-e az INP-t.
Az alábbi kód egy production-safe aggregátor, amit a saját RUM pipeline-omban használok. Csak azokat a frame-eket küldi vissza, amelyek harmadik fél szkriptet érintenek, és domain szinten csoportosítja őket:
// Third-party szkript attribúció aggregálása
const FIRST_PARTY = new Set([
location.hostname,
'cdn.mysite.com',
'assets.mysite.com'
]);
const thirdPartyCost = new Map(); // domain → total blocking ms
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
for (const script of entry.scripts) {
if (!script.sourceURL) continue;
let host;
try {
host = new URL(script.sourceURL).hostname;
} catch { continue; }
if (FIRST_PARTY.has(host)) continue;
const prev = thirdPartyCost.get(host) ?? 0;
thirdPartyCost.set(host, prev + script.duration);
}
}
}).observe({ type: 'long-animation-frame', buffered: true });
// 30 másodpercenként riportolás a saját endpointra
setInterval(() => {
if (!thirdPartyCost.size) return;
navigator.sendBeacon('/rum/loaf-thirdparty', JSON.stringify(
Array.from(thirdPartyCost.entries())
.map(([host, ms]) => ({ host, ms: Math.round(ms) }))
.sort((a, b) => b.ms - a.ms)
));
thirdPartyCost.clear();
}, 30_000);
A sendBeacon használata itt kritikus: ez az API akkor is elküldi az adatot, ha a felhasználó közben navigál el az oldalról. A Google Tag Manager-től eredő entry-k például általában a www.googletagmanager.com/gtm.js URL alatt jelennek meg, míg a Facebook Pixel a connect.facebook.net alatt. Ez alapján a marketing csapat felé is dokumentálható, hogy melyik tag mennyi milliszekundumba kerül a felhasználónak. (Ezt a riportot egy ügyfelemnél sikerült úgy elfogadtatni, hogy három tag-et végül kidobtak.)
LoAF integráció a web-vitals.js-szel és RUM-mal
A web-vitals.js 4.x már natívan integrálja a LoAF-ot: az onINP callback attribution módban visszaadja a leglassabb interakcióhoz tartozó LoAF entry-ket, így egyetlen mérésből mind a metrika (INP érték), mind a diagnózis (melyik script okozta) megvan. Ez egyben a legegyszerűbb módja annak, hogy a saját RUM pipeline-odba beleépítsd a LoAF adatokat:
A forcedStyleAndLayoutDuration mező különösen értékes: ez mutatja meg, ha egy szkript szinkron layout-ot kényszerített ki (pl. element.offsetHeight-ot olvasott közvetlenül DOM módosítás után). Ez a klasszikus „layout thrashing" mintázat, amit korábban csak a Chrome DevTools Performance panelben lehetett észrevenni. Éles környezetben most bármelyik felhasználó gépén detektálható, és ez a fajta adat vezetett minket egy közösen fejlesztett projekten már többször is ahhoz a felismeréshez, hogy egy pici debounce vagy egy requestAnimationFrame wrap oldotta meg a lassulást.
Böngészőtámogatás és fallback stratégia
2026 augusztusában a helyzet a következő. A Chromium-alapú böngészők (Chrome 123+, Edge 123+, Opera 109+, Brave, Arc, Samsung Internet 24+) mindegyike támogatja a Long Animation Frames API-t. A Safari (WebKit) és a Firefox jelenleg nem implementálja. A WebKit oldalán a spec „In Development" státuszban van, a Firefox-nál pedig még a dom.performance.long_animation_frame.enabled flag mögé sincs beépítve production build-ben.
Ez a valóságban nem probléma: az INP metrika 96%-ban Chromium felhasználóktól származik a CrUX 2026 Q2 adatai szerint, tehát a LoAF pontosan azon a platformon működik, ahol a legtöbb debug jelet szeretnénk kapni. A fallback logika viszont fontos, hogy ne dobjunk hibát Safari-n vagy Firefox-on:
// Feature detection + fallback
function observeLongFrames(callback) {
const types = PerformanceObserver.supportedEntryTypes ?? [];
if (types.includes('long-animation-frame')) {
// Elsődleges: LoAF (Chromium)
new PerformanceObserver((list) => {
for (const e of list.getEntries()) {
callback({
source: 'loaf',
duration: e.duration,
blocking: e.blockingDuration,
scripts: e.scripts.map((s) => ({
url: s.sourceURL,
ms: s.duration
}))
});
}
}).observe({ type: 'long-animation-frame', buffered: true });
return;
}
if (types.includes('longtask')) {
// Fallback: Long Task (Safari, régebbi böngészők)
new PerformanceObserver((list) => {
for (const e of list.getEntries()) {
callback({
source: 'longtask',
duration: e.duration,
blocking: e.duration - 50,
scripts: [] // nem elérhető
});
}
}).observe({ type: 'longtask', buffered: true });
return;
}
// Se LoAF, se Long Task, csendben nem csinálunk semmit
}
observeLongFrames((data) => {
// Egységes riportolás
});
Gyakori hibák LoAF elemzésnél
Az elmúlt évben több csapatnál is segítettem bevezetni a LoAF-ot, és három visszatérő hiba fordul elő, amelyek elrontják az elemzést. Az első, hogy a fejlesztők a duration mezőt egyenlővé teszik a „káros idővel", pedig ez tartalmazza a paint és compositing időt is, ami elkerülhetetlen. A tényleges „elkerülhető" idő a blockingDuration, ami csak azt méri, mennyivel volt hosszabb a frame a következő paint lehetőségnél. A második hiba, hogy csak a scripts[0]-t nézik. Egy 200 ms-os frame gyakran öt-hat script kombinációja, és a legnagyobb blokkoló nem mindig az első a tömbben. A helyes megközelítés a duration szerinti csökkenő sorrend.
A harmadik és talán legalattomosabb hiba a cross-origin szkriptek maszkolása. Ha egy külső domain szkriptje nem küld Timing-Allow-Origin: * fejlécet, a sourceURL és a sourceCharPosition mezők üresek lesznek (ez látszik invokerType: "classic-script" mellett is). Ilyenkor a vendor felé kell menni, hogy állítsák be a fejlécet, vagy proxyzd magadhoz a szkriptet. A Chrome for Developers LoAF dokumentációja részletesen leírja ezt az attribúciós modellt, és példát is ad a leggyakoribb SDK-król. (Anno én pont a Segment analytics szkriptjén akadtam el, mire rájöttem, hogy a TAO fejléc hiányzik.)
Végül egy figyelmeztetés. A fejlesztői környezetben (localhost, dev build) a LoAF adatok szinte használhatatlanul zajosak, mert a React DevTools, a Vite HMR és a source map betöltés mind extra frame-eket generál. A LoAF-ot mindig production build-en vagy legalább preview build-en profilozd, lehetőleg mid-range mobilon (Moto G Power vagy Pixel 6a) CPU throttling nélkül, mert a throttling maga is torzítja a blockingDuration értéket. Ezt egyébként az LCP optimalizálás gyakorlati útmutatóban is részleteztem az eszközválasztás kapcsán.
Gyakran Ismételt Kérdések
Mi a különbség a Long Animation Frame és egy Long Task között?
A Long Task egyetlen JavaScript feladatot mér, ami 50 ms-nál tovább tartott. A Long Animation Frame egy teljes renderelési ciklust mér (több task, layout, style és paint összesen), és részletes attribúciót ad script URL, invoker típus és blocking duration szinten.
Melyik böngészők támogatják a Long Animation Frames API-t 2026-ban?
A Chromium-alapú böngészők (Chrome 123+, Edge 123+, Opera, Brave, Arc, Samsung Internet 24+) mindegyike támogatja. A Safari és a Firefox 2026 augusztusában még nem implementálja, de a WebKit oldalán már „In Development" státuszban van.
Mekkora overhead-je van a LoAF observer-nek?
Minimális, mert csak azok a frame-ek keletkeztetnek entry-t, amelyek meghaladják az 50 ms-ot. Egy jól optimalizált oldalon percek is eltelhetnek egyetlen entry között, tehát a memória- és CPU-hatás elhanyagolható. Produkciós RUM pipeline-ban is biztonságosan használható.
Miért üres a sourceURL egyes script bejegyzéseknél?
Ez akkor fordul elő, ha a szkript cross-origin, és a szerver nem küld Timing-Allow-Origin: * fejlécet. A böngésző biztonsági okokból elrejti az URL-t. Kérj a vendortól TAO fejlécet, vagy proxyzd magadhoz a fájlt.
Használhatom a LoAF-ot INP debug-hoz éles környezetben?
Igen, ez pontosan az elsődleges use case-e. A web-vitals.js 4.x onINP attribution módja már automatikusan csatolja a leglassabb interakcióhoz tartozó LoAF entry-ket, így minden 200 ms fölötti INP mérés mellé megkapod a felelős script URL-jét és a blocking duration-t.
Hogyan mérd és javítsd a bfcache hit rate-et 2026-ban: NotRestoredReasons API, Cache-Control: no-store, unload vs pagehide, WebSocket és IndexedDB kezelés, mezei RUM adatok és CrUX lekérdezés lépésről lépésre.