Long Animation Frames API (LoAF) este o interfață browser stabilă din Chrome 123 care raportează cadre de animație depășind 50 ms și, mai important, îți dă atribution la scriptul exact care a cauzat blocajul. Vechiul Long Tasks API îți spunea doar că ceva a fost lent. În 2026, LoAF este instrumentul principal pe care îl folosesc în producție ca să diagnostichez INP slab. Practic, îmi arată dacă vinovatul e un handler propriu, un script terț, un rendering costisitor sau un forced reflow. Fără LoAF, RUM-ul îți spune „INP-ul e prost”; cu LoAF, îți spune „linia 42 din analytics.js e problema”.
LoAF raportează orice cadru de animație mai lung de 50 ms, cu breakdown pe script, render, style și layout.
Suport stabil în Chrome, Edge și Opera (v123+). Safari și Firefox sunt în evaluare, deci LoAF acoperă aproximativ 70% din traficul global din CrUX.
Câmpul scripts[] conține invoker, sourceURL, sourceCharPosition și sourceFunctionName. Ai atribution la nivel de linie.
Corelezi PerformanceLongAnimationFrameTiming cu evenimentul INP folosind startTime și duration ca să identifici cadrul care a cauzat interacțiunea lentă.
Overhead-ul măsurat de echipa Chrome e sub 1 ms per cadru. E sigur pentru producție, chiar și pe mobile mid-range.
Librăria web-vitals v4+ integrează LoAF în atribution-ul INP automat prin onINP(cb, {reportAllChanges: true}).
Ce este Long Animation Frames API?
Long Animation Frames API e o extensie a interfeței PerformanceObserver care raportează fiecare cadru de animație a cărui procesare a depășit 50 ms. Un „cadru de animație” în terminologia LoAF înseamnă tot ciclul de la începerea procesării unui task (execuția JavaScript-ului) până la afișarea pixelilor pe ecran. Include deci scripting, style, layout, paint și compositing. Dacă cadrul depășește pragul, browserul consideră că interacțiunile utilizatorului vor părea lente și înregistrează un obiect PerformanceLongAnimationFrameTiming.
Diferența față de Long Tasks API e că LoAF îți dă atribution la scripting: ce funcție a rulat, din ce fișier, la ce offset. În experiența mea de RUM din ultimele 12 luni, LoAF a redus timpul mediu de investigare a unui incident INP de la vreo 4 ore (cu Long Tasks plus Chrome DevTools la nevoie) la aproximativ 20 de minute (LoAF direct în dashboard). Specificația oficială e publicată de W3C Web Performance Working Group. Vezi draft-ul curent al specificației LoAF pentru câmpurile complete și starea implementării.
API-ul a devenit stabil în Chrome 123 (martie 2024) și, până la mijlocul lui 2026, e disponibil în toate browserele Chromium: Chrome, Edge, Opera, Samsung Internet, Brave. Safari a semnalat interes pozitiv, iar Firefox a marcat ticket-ul ca „worth prototyping”. Practic, acoperi peste două treimi din traficul global fără polyfill.
LoAF vs Long Tasks API: ce s-a schimbat?
Long Tasks API (disponibil din 2017) raporta orice task mai lung de 50 ms pe main thread, dar avea trei limitări fatale pentru debugging INP. În primul rând, nu includea forced synchronous layout, așa că reflow-urile costisitoare erau invizibile. În al doilea rând, nu avea atribution utilizabilă: câmpul attribution întorcea aproape întotdeauna "unknown" sau "same-origin", fără sursă. În al treilea rând, un task de 40 ms urmat imediat de un altul de 40 ms nu era raportat, deși cumulat blocau frame-ul.
Tabela de mai jos rezumă diferențele pe care le-am simțit cel mai des în producție.
Aspect
Long Tasks API
Long Animation Frames API
Prag de raportare
Task > 50 ms
Cadru animație > 50 ms
Include forced reflow
Nu
Da (câmp styleAndLayoutStart)
Include render + paint
Nu
Da (renderStart, paintTime)
Sursă script identificată
Nu (opac)
Da (URL + funcție + offset)
Task-uri consecutive
Ratate
Agregate în același frame
Suport browser (2026)
Chrome, Edge, Firefox
Chrome, Edge, Opera, Samsung
Overhead măsurat
~0.3 ms/task
~0.8 ms/frame
Concluzia mea, pe scurt: pentru diagnosticarea INP-ului, Long Tasks e suficient doar cât să știi „e ceva lent”. Pentru „ce anume și unde”, ai nevoie de LoAF. Long Tasks rămâne util doar dacă țintești browsere fără suport LoAF, iar chiar și acolo aș recomanda să-l tratezi ca fallback secundar. Am migrat toate dashboard-urile RUM proprii pe LoAF ca sursă primară încă din Q3 2024.
Cum implementezi un PerformanceObserver pentru LoAF
Setup-ul minimal are trei linii utile. Îl pornești cât mai devreme posibil în <head>, înainte de orice script terț, ca să captezi cadrele lente din timpul boot-ului. Codul de mai jos e ce rulez eu în producție pe site-urile clienților, cu logging condiționat pentru sampling.
// Observer LoAF cu sampling la 10% din sesiuni
if (typeof PerformanceObserver !== 'undefined' &&
PerformanceObserver.supportedEntryTypes?.includes('long-animation-frame')) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry este PerformanceLongAnimationFrameTiming
const report = {
duration: entry.duration,
renderStart: entry.renderStart,
styleAndLayoutStart: entry.styleAndLayoutStart,
blockingDuration: entry.blockingDuration,
scripts: entry.scripts.map(s => ({
name: s.name,
duration: s.duration,
invoker: s.invoker,
invokerType: s.invokerType,
sourceURL: s.sourceURL,
sourceFunctionName: s.sourceFunctionName,
sourceCharPosition: s.sourceCharPosition,
forcedStyleAndLayoutDuration: s.forcedStyleAndLayoutDuration
}))
};
// Trimite la endpoint-ul RUM propriu
navigator.sendBeacon('/rum/loaf', JSON.stringify(report));
}
});
observer.observe({ type: 'long-animation-frame', buffered: true });
}
Opțiunea buffered: true e critică. Fără ea pierzi cadrele lente care s-au întâmplat înainte ca observer-ul tău să pornească, iar de regulă primele cadre lente sunt cele mai importante, pentru că se corelează cu INP-ul din startup. Nu uita nici de navigator.sendBeacon în loc de fetch. Beacon-ul supraviețuiește la visibilitychange, ceea ce contează pe mobile, unde utilizatorul închide tab-ul instant.
Un detaliu pe care l-am ratat inițial: PerformanceObserver primește cadre în batch-uri, nu unul câte unul. Dacă utilizatorul deschide o pagină cu 20 de cadre lente în primele 5 secunde, vei primi un singur callback cu 20 de intrări. Iterează întotdeauna cu getEntries(), nu doar cu list[0]. Pentru o abordare mai atentă a job-urilor blocante și cum le eviți în primul rând, vezi ghidul de optimizare bundle JavaScript pentru INP rapid.
Atribution la nivel de script: câmpul scripts[]
Puterea reală a LoAF stă în array-ul scripts. Fiecare intrare descrie un script care a rulat în timpul cadrului, cu contextul complet. Câmpurile cheie:
invokerType: cum a fost declanșat: event-listener, user-callback, promise-resolve, classic-script, module-script, observer-callback. Diferențiază între „utilizatorul a apăsat un buton” și „un microtask s-a rezolvat”.
invoker: string descriptiv al declanșatorului. Pentru un event listener pe un buton arată ceva de tipul BUTTON#submit.onclick.
sourceURL: URL-ul fișierului. Dacă e inline sau eval, va fi URL-ul documentului cu offset.
sourceFunctionName: numele funcției care a fost pe stack top. Dacă e minified, obții numele minified (motiv să încarci source maps în tool-ul de RUM).
sourceCharPosition: offset caracter în sursă. Cuplat cu source map, îți dă linia și coloana originală.
forcedStyleAndLayoutDuration: timp petrecut într-un forced reflow (citirea offsetTop, getBoundingClientRect etc. între mutații DOM). Aici sunt majoritatea surprizelor.
pauseDuration: timp în care scriptul a fost blocat sincron (alert, prompt, synchronous XHR).
Pe unul dintre proiectele mele, un handler de click pe un dropdown avea INP-ul p75 la 380 ms. LoAF a arătat că scriptul propriu rula 12 ms, dar forcedStyleAndLayoutDuration era 340 ms. Cauza: un getBoundingClientRect() apelat într-un loop peste 200 de itemi. Cu Long Tasks nu am fi văzut niciodată asta, pentru că reflow-urile sincrone nu apar ca task-uri separate. Vezi și documentația MDN pentru PerformanceLongAnimationFrameTiming pentru lista completă a câmpurilor și exemple de payload.
Cum corelezi LoAF cu evenimentele INP
INP este cea mai lungă interacțiune (cu marjă pentru outliers) din întreaga vizită. Când RUM-ul îți raportează un INP de 480 ms, ai nevoie să știi ce cadru anume a cauzat asta. Corelarea se face pe timeline: iei entry.startTime și entry.startTime + entry.duration din evenimentul INP (via event entry type) și cauți cadre LoAF care se suprapun cu acest interval.
// Buffer LoAF frames din ultimele 30 s
const recentFrames = [];
const MAX_AGE_MS = 30_000;
new PerformanceObserver((list) => {
for (const frame of list.getEntries()) {
recentFrames.push(frame);
}
// Cleanup frames vechi
const cutoff = performance.now() - MAX_AGE_MS;
while (recentFrames.length && recentFrames[0].startTime < cutoff) {
recentFrames.shift();
}
}).observe({ type: 'long-animation-frame', buffered: true });
// Când primești un eveniment INP, găsește cadrul suprapus
function findLoafForInp(inpEntry) {
const inpStart = inpEntry.startTime;
const inpEnd = inpStart + inpEntry.duration;
return recentFrames.find(f => {
const fEnd = f.startTime + f.duration;
return f.startTime <= inpEnd && fEnd >= inpStart;
});
}
Din Chrome 129, această corelare se face automat dacă folosești librăria web-vitals v4+ cu onINP(callback, {reportAllChanges: true}). Obiectul primit conține attribution.longAnimationFrameEntries, care e exact array-ul de cadre LoAF suprapuse cu interacțiunea. Nu mai trebuie să implementezi manual corelarea decât dacă ai constrângeri speciale (de exemplu, RUM propriu fără dependințe externe).
Integrare cu web-vitals și RUM propriu
Ecosistemul de RUM din 2026 tratează LoAF ca cetățean de rangul întâi. Librăria oficială Google web-vitals (v4.2+) expune automat atribution-ul LoAF pentru INP:
Payload-ul rezultat e mic (sub 1 KB tipic) și îți dă direct în dashboard „cine e vinovat” pentru fiecare eveniment INP slab. La un client de e-commerce, această integrare a redus MTTR-ul incidentelor de performanță de la 2 zile la 3 ore. Vezi ghidul web.dev pentru INP și atribution pentru contextul complet.
Dacă ai deja un RUM comercial (Datadog, New Relic, SpeedCurve, Akamai mPulse), verifică changelog-ul lor. Majoritatea au adăugat suport LoAF în 2025. Pentru dashboard-uri custom, un simplu Grafana peste un Postgres cu payload JSONB funcționează excelent și îți costă zero per month. Pentru context suplimentar despre metricile de performanță și cum le lecturi corect, vezi ghidul complet de optimizare INP din 2026.
Exemple reale de INP slab diagnosticate cu LoAF
Trei incidente pe care le-am rezolvat în 2025-2026 folosind exclusiv LoAF, ca să vezi cum arată în practică.
1. Analytics terț care rula pe fiecare interacțiune
Un site de conținut avea INP p75 la 320 ms pe articole. LoAF a arătat consistent scripts cu invokerType: 'event-listener', sourceURL pointând la CDN-ul unui vendor de analytics, cu durata 180-220 ms per interacțiune. Soluția: mutat listener-ul pe requestIdleCallback și amânat inițializarea vendor-ului până după prima interacțiune (LCP + 2s). INP p75 a scăzut la 140 ms în 48 de ore.
2. Framework re-render integral pe click
O aplicație React cu state global masiv triggera re-render pe tot arborele la fiecare click. LoAF a arătat invoker: 'BUTTON.tab-btn.onclick' urmat de sourceFunctionName: 'ReactDOMHydrationRoot.render' cu 260 ms de scripting și 45 ms în layout. Fix: memoization cu useMemo pe listele mari și mutarea state-ului în context-uri separate. Un exemplu paralel de optimizare bundle e detaliat în ghidul JavaScript menționat mai sus.
3. Forced reflow într-un carousel
Un carousel de produse citea element.clientWidth pentru fiecare slide într-un loop, între mutații de style.transform. LoAF a arătat forcedStyleAndLayoutDuration: 190ms pe un singur script. Fix: caching-ul lățimii într-o variabilă înainte de loop. INP p75 a scăzut de la 410 ms la 95 ms. Honestly, e genul de bug pe care l-am urmărit ore întregi în DevTools înainte să pornesc LoAF; acum îl văd la primul screenshot din dashboard.
Capcane comune în producție
Câteva lucruri pe care le-am învățat greu:
Source maps sunt obligatorii. Fără ele, sourceFunctionName îți dă t sau a1 și n-ai idee ce funcție e. Încarcă source maps în tool-ul de RUM și rezolvă offset-urile server-side.
Sampling atent. Un site cu 10M page views pe zi și 5% cadre lente per pagină generează 500K entries/zi. La 1 KB fiecare, e 500 MB. Suportabil, dar nu ieftin. Sample la 5-10% din sesiuni, nu la 100%.
PII în URL. Dacă un script vine dintr-un URL cu query params ce conțin date personale, LoAF îl raportează ca atare. Sanitizează sourceURL înainte de trimitere.
Chrome pre-123 nu are LoAF. Verifică întotdeauna supportedEntryTypes.includes('long-animation-frame') ca să nu arunci excepții pe Safari/Firefox.
Nu confunda blockingDuration cu duration.duration include tot cadrul, inclusiv timpul „safe” (idle între task-uri). blockingDuration e ce contează pentru INP: timpul cumulat al task-urilor >50ms din cadru.
Întrebări frecvente
Ce browsere suportă Long Animation Frames API în 2026?
Chrome 123+, Edge 123+, Opera 109+, Samsung Internet 25+ și Brave. Safari a semnalat interes pozitiv (etichetă „Position: worth prototyping”), iar Firefox îl are în implementer interest. Practic, LoAF acoperă peste 70% din traficul web global în 2026.
Care este diferența dintre Long Animation Frames și Long Tasks?
Long Tasks raportează task-uri individuale peste 50 ms pe main thread, fără atribution utilizabilă și fără forced reflow. LoAF raportează cadre de animație complete cu atribution la scripting (URL, funcție, offset), include reflow-uri sincrone și agregă task-uri consecutive. Pentru debugging INP, LoAF este net superior.
Pot folosi LoAF în producție fără impact pe performanță?
Da. Echipa Chrome a măsurat overhead-ul la aproximativ 0.8 ms per cadru raportat, iar cadrele lente sunt prin definiție rare pe pagini optimizate. Cu sampling la 10% din sesiuni și navigator.sendBeacon, impactul pe CPU și rețea e neglijabil chiar și pe dispozitive mid-range.
Cum identific scriptul exact care cauzează un INP slab?
Folosește onINP(callback, {reportAllChanges: true}) din web-vitals v4+. Callback-ul primește attribution.longAnimationFrameEntries, un array de cadre LoAF suprapuse cu interacțiunea. Sortează după duration și inspectează sourceURL plus sourceFunctionName.
LoAF funcționează în iframes și Service Workers?
LoAF funcționează în orice document top-level și în iframes same-origin. În iframes cross-origin este dezactivat din motive de privacy. În Service Workers și Web Workers nu e disponibil, pentru că nu există „animation frame” în afara main thread-ului cu DOM.
Ghid practic de optimizare a fonturilor web în 2026. WOFF2, subsetting, font-display, metric overrides pentru zero CLS, self-hosting vs Google Fonts, preloading, Fontaine, variable fonts și service worker cache. Cu exemple de cod aplicabile imediat.
TTFB e metrica-fundament a performanței web. Ghid practic de optimizare în 2026: diagnosticare cu Server-Timing, caching multi-nivel, CDN cu stale-while-revalidate, 103 Early Hints, edge computing și Streaming SSR cu React 19.
Ghid practic de optimizare CLS în 2026. Identifică și rezolvă cauzele layout shifts — imagini fără dimensiuni, fonturi web, reclame, animații — cu cod funcțional și tehnici precum CSS Containment, content-visibility și font metric overrides.