Long Animation Frames API: debugujte INP v roce 2026
Long Animation Frames API v Chromu 123+ konečně dává přesnou atribuci k dlouhým rámcům: který skript a která funkce vám dělá špatné INP. Kód pro PerformanceObserver, atribuci scripts[] a fixy přes scheduler.yield() nebo Web Workers.
Long Animation Frames API (LoAF) je webové rozhraní dostupné od Chrome 123, které reportuje rámce delší než 50 ms včetně přesné atribuce k <script>, invokátoru, práci ve stylech a v layoutu. V roce 2026 je to jediný spolehlivý způsob, jak v produkci zjistit, který kus kódu vám dělá špatné INP. V tomhle článku ukážu, jak LoAF číst v PerformanceObserver, jak výsledky posílat do RUM, jak najít viníka a jak dlouhé rámce rozbít pomocí scheduler.yield(), Web Workers a lazy hydratace.
Long Animation Frames API reportuje rámce > 50 ms včetně skript-level atribuce (URL, funkce, invokátor). Long Tasks API tuto úroveň detailu nikdy neměla.
LoAF je stabilní od Chrome 123 (duben 2024) a od Chrome 130 zahrnuje i práci v layoutu, stylech a paintu. Ve Firefoxu a Safari k roku 2026 stále chybí, takže mějte fallback na Long Tasks API.
Každý dlouhý rámec obsahuje pole scripts[] s invoker, invokerType, sourceURL, sourceFunctionName a sourceCharPosition. Přesně to, co potřebujete v post-mortem trace.
Rámce nad 200 ms téměř vždy zabíjejí INP; hranice pro dobré INP je 200 ms podle web.dev.
Nejúčinnější fixy: scheduler.yield() uvnitř dlouhých smyček, scheduler.postTask() pro prioritu, přesun čisté logiky do Web Workeru a odložená hydratace.
LoAF entries jsou levné. V produkci můžete sbírat 100 % relací, není třeba samplování jako u Long Tasks.
Co je Long Animation Frames API?
Long Animation Frames API (zkracováno LoAF) je nová performance API dostupná přes standardní PerformanceObserver. Reportuje kompletní rámce, od začátku vstupní události nebo timeru až po dokončení renderu, pokud trvaly déle než 50 milisekund. Kritický rozdíl proti staré Long Tasks API je v tom, že LoAF měří rámec, ne izolovanou task. Takže vidíte kumulativní cenu několika krátkých tasků, které dohromady blokovaly render.
Podle oficiální Chrome dokumentace je API stabilní od Chrome 123 (duben 2024) a od Chrome 130 obsahuje i atribuci k práci ve stylech, layoutu a paintu, ne jen ke skriptům. V praxi to znamená, že když vám getBoundingClientRect() vyvolá synchronní reflow, LoAF vám to řekne stejně dobře jako když vám 300 ms běží JSON parser.
Každý PerformanceLongAnimationFrameTiming entry obsahuje pět klíčových časových značek: startTime (kdy začala první task rámce), renderStart (kdy začal render fáze), styleAndLayoutStart, firstUIEventTimestamp a duration. Pole scripts[] pak rozepisuje jednotlivé skript-level příspěvky s přesnou atribuční informací (víc o tom v sekci atribuce).
Long Animation Frames vs Long Tasks: v čem je rozdíl?
Long Tasks API existuje od roku 2018 a reportuje jednotlivé JavaScriptové tasky delší než 50 ms. Problém: v moderních aplikacích už jsou tasky rozstřihané na malé kousky pomocí setTimeout(0) nebo await, takže Long Tasks API vidí čtyři 45ms tasky za sebou jako "čtyři krátké tasky", přestože uživatel čeká 180 ms na první frame. LoAF sečte tyto tasky do jednoho rámce a upozorní vás na skutečný problém.
Vlastnost
Long Tasks API
Long Animation Frames API
Jednotka měření
Jednotlivá JS task
Celý animation frame
Práh
> 50 ms
> 50 ms
Atribuce ke skriptu
Minimální (jen origin)
Plná: URL, funkce, řádek, invoker
Práce v layoutu a paintu
Ne
Ano (Chrome 130+)
Rendering start
Nedostupný
renderStart, styleAndLayoutStart
Zachytí rozstřižené tasky
Ne
Ano
Podpora prohlížečů (2026)
Chrome, Edge, Safari, Firefox
Chrome, Edge (Safari/Firefox v přípravě)
Vhodné pro produkci
Ano, ale málo actionable
Ano, plně actionable
Když debuguji INP regresi, dívám se nejprve do LoAF na dlouhé rámce v okolí interakce. Long Tasks jsou dnes už jen fallback pro Firefox a Safari, v Chrome je nepoužívám. Podobný postup jsem popsal v článku jak opravit špatné INP skóre, ale bez LoAF byla atribuce viníka výrazně těžší. Honestly, přechod na LoAF byl asi největší kvalitativní skok v RUM datech, který jsem za posledních pár let zažil.
Jak zachytit LoAF entries v kódu
Registrace observeru je stejná jako u jiných performance API. Používáte PerformanceObserver s typem "long-animation-frame". Nezapomeňte na buffered: true, jinak přijdete o rámce, které nastaly před inicializací observeru (což je typicky celé bootovací období aplikace).
// Zachytávání LoAF entries — spusťte co nejdříve v <head>
if ('PerformanceObserver' in window &&
PerformanceObserver.supportedEntryTypes?.includes('long-animation-frame')) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Ignoruj rámce pod 100 ms — 50-100 ms je běžný šum
if (entry.duration < 100) continue;
console.log('Long frame:', {
duration: entry.duration.toFixed(1) + ' ms',
blockingDuration: entry.blockingDuration.toFixed(1) + ' ms',
renderStart: entry.renderStart,
styleAndLayoutStart: entry.styleAndLayoutStart,
scripts: entry.scripts.map(s => ({
name: s.name,
duration: s.duration.toFixed(1),
invoker: s.invoker,
sourceURL: s.sourceURL,
sourceFunctionName: s.sourceFunctionName,
sourceCharPosition: s.sourceCharPosition
}))
});
}
});
observer.observe({ type: 'long-animation-frame', buffered: true });
}
Pole blockingDuration je zvlášť užitečné: reportuje čas, kdy byl main thread blokovaný nad 50 ms nad limitem "idle". Pokud vás zajímá jen ta část, která reálně zpožďuje reakci uživatele, filtrujte podle blockingDuration > 0.
Atribuce: který skript blokoval frame
Skript-level atribuce je hlavní důvod, proč LoAF používat. Každá entry má pole scripts[], kde každý objekt popisuje jeden JavaScriptový příspěvek do rámce. Zajímají mě tyto vlastnosti:
invoker, třeba "BUTTON#submit.onclick", "setTimeout", "IMG.onload"
invokerType, jedna z hodnot user-callback, event-listener, resolve-promise, reject-promise, classic-script, module-script
sourceURL, URL skriptu, který funkci definoval (často je to bundle chunk)
sourceFunctionName, jméno funkce; u anonymních callbacků prázdné
sourceCharPosition, pozice ve zdroji, dá se párovat se sourcemapou
pauseDuration a forcedStyleAndLayoutDuration, pokud vaše funkce vynutila synchronní layout (například čtením offsetWidth), tady to uvidíte
V praxi si to zjednodušuji tak, že si u každého dlouhého rámce vypíšu top-3 skripty podle duration a hned vidím viníka. Pokud je forcedStyleAndLayoutDuration vysoké, jde o layout thrashing. V tom případě je řešení jinde než v pouhém rozstřižení tasku.
Mapování sourceCharPosition na sourcemapu
V produkčním buildu ukazuje sourceURL na minifikovaný chunk. Pro mapování zpět na TypeScript zdroj používám source-map library od Mozilly. Stačí načíst .js.map a zavolat consumer.originalPositionFor({ line: 1, column: sourceCharPosition }). LoAF vždy reportuje pozici jako absolutní offset, takže line: 1 je bezpečná volba pro bundle jednorámcové.
Jak opravit dlouhé animation frames?
Jakmile identifikujete konkrétní funkci, která žere main thread, máte v roce 2026 čtyři hlavní páky: scheduler.yield(), scheduler.postTask(), Web Workers a odloženou hydrataci. Pořadí je záměrné, začínejte tou nejjednodušší.
scheduler.yield() uvnitř smyček
Nová Scheduler API (stabilní v Chromium od verze 129) přinesla scheduler.yield(), asynchronní await, který vrátí kontrolu event loopu, zpracuje čekající vstupy a pak pokračuje. Rozdíl proti setTimeout(0) je zásadní: yield() má vyšší prioritu než nová task fronta, takže vaše smyčka není odsouvána na konec.
async function processItems(items) {
for (let i = 0; i < items.length; i++) {
doWork(items[i]);
// Každých 50 iterací uvolni thread pro user input
if (i % 50 === 0 && 'scheduler' in window && 'yield' in scheduler) {
await scheduler.yield();
}
}
}
Fallback pro Safari a Firefox (které v roce 2026 scheduler.yield() stále nemají):
const yieldToMain = () => {
if ('scheduler' in window && 'yield' in scheduler) {
return scheduler.yield();
}
return new Promise(resolve => setTimeout(resolve, 0));
};
Prioritizace pomocí scheduler.postTask()
scheduler.postTask() je ještě flexibilnější. Plánuje task s prioritou user-blocking, user-visible nebo background. Používám ji pro analytiku a telemetrii, aby nikdy nekonkurovala UI updatům:
Pokud jde o čistou logiku (parsování, výpočet, komprese), přesuňte ji do Web Workeru. Uvidíte, že rámec na main threadu klesne z 300 ms na 5 ms. Všechno ostatní se dělo v pozadí. Nezapomeňte na Transferable objekty pro velké ArrayBuffer, jinak vás bude bolet cena serializace. Já jsem tenhle bug zažil naposledy, když jsem v jednom projektu forgot to transfer a hlavní vlákno se sekalo na `structuredClone` skoro tak dlouho jako celý původní výpočet.
Odložená hydratace komponent
Pokud LoAF ukáže, že dlouhé rámce se dějí během initial hydratace, řešením je hydrataci rozstřihat. V React Server Components (React 19) lze používat <Suspense> s postupným ostrovým vzorem, o tom podrobně píšu ve streaming SSR průvodci. V Astru nebo Qwiku máte client:idle a client:visible.
Jak LoAF souvisí s INP skóre?
Interaction to Next Paint (INP) měří čas od uživatelské interakce po další paint. Long animation frame, který nastane během této doby, přímo tlačí INP nahoru. S LoAF entries a firstUIEventTimestamp můžete zpětně sestavit trace od inputu k paintu.
Podle web.dev metriky INP je hranice pro "good" 200 ms. To znamená, že v Chrome 130+ vám každý rámec s duration > 200 ms, který obsahuje firstUIEventTimestamp, prakticky garantovaně shodí INP. Filtrujte LoAF entries takto:
observer.observe({ type: 'long-animation-frame', buffered: true });
// V callbacku:
for (const entry of list.getEntries()) {
const hadUIEvent = entry.firstUIEventTimestamp > 0;
const overINPBudget = entry.duration > 200;
if (hadUIEvent && overINPBudget) {
reportINPCulprit(entry); // Přesně tyto rámce chcete lovit
}
}
Google's web-vitals library od verze 4 automaticky připojuje LoAF atribuci k INP eventům. Pokud sbíráte INP pomocí onINP() s reportAllChanges: true, dostanete attribution.longAnimationFrameEntries zdarma.
Odesílání LoAF do RUM systému
V produkci nechci logovat každý rámec. Sbírám jen ty, které jsou nad 150 ms nebo mají blockingDuration > 100. Odesílám je přes navigator.sendBeacon() během visibilitychange, ne v reálném čase.
Payload zůstává pod 4 KB i pro 20+ rámců, takže sendBeacon se nikdy neodmítne. Na backendu pak agreguji podle sourceURL a sourceFunctionName. Top-10 nejdražších funkcí v aplikaci je vždy překvapivě krátký seznam, obvykle stačí opravit dvě z nich a p95 INP se propadne o třetinu.
Podpora prohlížečů a fallbacky
V září 2026 vypadá podpora takhle: Chromium (Chrome, Edge, Opera, Brave, Arc) od verze 123 mají plnou podporu. Safari implementaci v roce 2026 stále nemá; Apple to má na positions/mozilla jako "neutral". Firefox má issue #1859041 se statusem "in progress" od poloviny 2025, ale žádný release ETA.
Pro Safari a Firefox používám fallback na Long Tasks API. Méně detailu, ale aspoň zachytím výskyt problému:
const supportsLoAF = PerformanceObserver.supportedEntryTypes
?.includes('long-animation-frame');
if (supportsLoAF) {
observeLoAF();
} else if (PerformanceObserver.supportedEntryTypes?.includes('longtask')) {
observeLongTasks(); // Fallback bez atribuce
}
Zajímavý side-benefit: protože LoAF pokrývá jen Chromium, ale Chromium má ~70 % desktop trhu a ~65 % mobile, i s tímto omezením pokrýváte většinu produkčních uživatelů. Když ještě přidáte třídění podle geografie a device tier, dostanete se k realistickým 2026 metrikam mnohem rychleji než čekáním na Safari.
Časté otázky
Co je Long Animation Frames API a k čemu slouží?
Long Animation Frames API je performance rozhraní, které reportuje kompletní rámce delší než 50 ms i s atribucí ke konkrétním skriptům, funkcím a jejich invokátorům. Slouží především k diagnostice INP a Total Blocking Time v produkci, kde tradiční profilování z DevTools není dostupné.
Jaký je rozdíl mezi Long Tasks API a Long Animation Frames API?
Long Tasks API měří jednotlivé JavaScriptové tasky nad 50 ms bez detailní atribuce. Long Animation Frames API měří celý rámec (sečtení všech tasků od inputu po paint) a přidává skript-level atribuci: URL, funkci, řádek, invoker i práci v layoutu a paintu. LoAF je téměř vždy užitečnější.
Jak vyřešit dlouhé animation frames v Reactu nebo Next.js?
Rozdělte hydrataci pomocí <Suspense> a Server Components (React 19), použijte scheduler.yield() ve smyčkách, přesuňte čistou logiku do Web Workeru a odložte third-party skripty. LoAF vám ukáže, která z těchto páků má největší dopad; sledujte pole scripts[] a filtrujte podle sourceURL.
Funguje Long Animation Frames API ve Firefoxu a Safari?
K září 2026 ne. Chrome, Edge a další Chromium prohlížeče podporují LoAF od verze 123 (duben 2024). Firefox má funkci ve fázi vývoje bez ETA, Safari ji nemá na plánu. Pro tyto prohlížeče použijte fallback na Long Tasks API nebo zapněte sběr jen na Chromium.
Jak měřit INP pomocí Long Animation Frames API?
Filtrujte LoAF entries podle firstUIEventTimestamp > 0 (rámec obsahuje user input) a duration > 200 (překračuje INP budget). Alternativně použijte Google web-vitals library verze 4+, která atribuci LoAF automaticky přikládá k INP eventům přes onINP().
Je bezpečné sbírat LoAF v produkci pro všechny uživatele?
Ano. LoAF má nízkou režii; reportuje pouze rámce nad 50 ms a atribuce je připravená v době, kdy Chrome frame renderuje. Můžete sbírat 100 % relací bez samplování. Payload udržte pod 4 KB a odesílejte přes navigator.sendBeacon() při visibilitychange.
Vlastnost content-visibility: auto zkrátí render dlouhé stránky o desítky procent. Ukážeme si contain-intrinsic-size, rozdíl mezi auto a hidden, CLS pasti a jak vše změřit v Chrome DevTools.
Streaming SSR v React 19 a Next.js 16 nezrychluje vždy. Ukážeme, kdy pomůže LCP, kdy uškodí, jak umístit Suspense boundaries a proč Cloudflare v proxy módu často stream úplně zabije. Praktické tipy z reálných auditů plus příklady pro Node i Edge.
Kompletní průvodce optimalizací webových fontů v roce 2026. Self-hosting WOFF2, preload, subsetting přes unicode-range, variable fonts a size-adjust pro nulový CLS, včetně praktických ukázek kódu z reálných auditů.