Long Animation Frames API: Debug dell'INP in produzione

Un playbook di produzione per trovare lo script che ha interrotto la tua interazione

Arjen Karel Core Web Vitals Consultant
Arjen Karel - linkedin
Last update: 2026-04-30

Per anni la risposta a "cosa sta causando il mio INP" è stata spesso uno sguardo vuoto. La Long Tasks API ti diceva che il thread principale era bloccato per più di 50ms. Non ti diceva quale script. Non ti diceva quale riga. Tutto ciò che avevamo era un numero in ms.

La Long Animation Frames API risolve questo problema. È disponibile nei browser Chromium a partire da Chrome 123 e ti offre l'anatomia completa di un frame: durata totale, durata del blocco, il momento in cui è iniziato il renderer, il momento in cui sono iniziati lo stile e il layout, e un array di ogni script che è stato eseguito nel frame per più di 5ms. Ogni voce di script include l'URL di origine, il nome della funzione, la posizione del carattere nel file e il tipo di callback che l'ha invocato.

Questa pagina fa parte della nostra serie sull'Interaction to Next Paint (INP). Il playbook LoAF di seguito è ciò che eseguo dopo aver diagnosticato la metrica. Se sei nuovo ad INP, inizia con le guide in tre fasi per l'input delay, il processing time, e il presentation delay, e con il flusso di lavoro più ampio per trovare e risolvere i problemi INP.

Perché Long Tasks non era sufficiente

Un task lungo si attiva quando un singolo task sul thread principale dura più di 50ms. Questo è utile per intercettare i blocchi evidenti. È inutile per l'INP perché la maggior parte delle interazioni lente non è un unico task lungo. Sono una sequenza di task medi più un rendering lungo.

Un'interazione può fallire l'INP senza mai attivare un task lungo. Un gestore di eventi da 40ms seguito da un ricalcolo dello stile di 70ms si somma a 110ms di presentation delay. La Long Tasks API non riporta nulla. L'utente percepisce il ritardo.

L'altro fallimento di Long Tasks è l'attribuzione. Ottieni un tipo di contenitore, un nome di contenitore e un campo nome che ti dice se il task lungo era "self", "same-origin-ancestor", "same-origin-descendant", "unknown", o una di alcune varianti cross-origin. Non ottieni lo script che è stato eseguito. Quindi, anche quando intercetti un task lungo, devi comunque aprire i DevTools e registrare di nuovo l'interazione per trovarne la causa. Questo funziona in laboratorio. Non funziona per l'INP su utenti reali che non puoi riprodurre.

LoAF sostituisce entrambe queste limitazioni. Cattura l'intero frame, non un singolo task. E ti dice esattamente quale script è stato eseguito, tramite URL e nome della funzione.

I sette campi che contano

Una voce LoAF ha più campi del necessario. Questi sette sono quelli che leggo per primi quando eseguo il debug di un'interazione.

duration è la lunghezza totale del frame in millisecondi. Una voce LoAF viene creata solo quando supera i 50ms. Questo numero da solo ti dice se il frame è un problema, ma non il perché.

blockingDuration è più utile. Somma le parti del frame che hanno superato la soglia del task lungo, sottraendo 50ms da ciascuna. Un frame con duration: 320 e blockingDuration: 0 è principalmente lavoro di rendering. Un frame con duration: 320 e blockingDuration: 270 è uno script lungo. Il primo necessita di una correzione di rendering. Il secondo necessita di una modifica al codice.

renderStart è il timestamp in cui il browser ha iniziato a eseguire il lavoro di rendering per il frame. Tutto ciò che è compreso tra startTime e renderStart è l'esecuzione dello script. Tutto il resto è stile, layout e paint. Questo è il limite più utile in assoluto per il debug dell'INP perché ti dice se il collo di bottiglia è JavaScript o il rendering.

styleAndLayoutStart è il timestamp in cui il rendering è passato dall'esecuzione dei callback dei frame di animazione all'effettivo ricalcolo dello stile e del layout. Il divario tra renderStart e styleAndLayoutStart sono le tue callback di requestAnimationFrame. Il divario tra styleAndLayoutStart e la fine del frame è il lavoro di stile e layout lato browser.

firstUIEventTimestamp è il momento in cui è arrivato un input dell'utente durante il frame. Se questo valore è diverso da zero, il frame contiene un'interazione. In questo modo web-vitals.js correla le voci LoAF con le misurazioni dell'INP. L'assenza di firstUIEventTimestamp significa che il frame è lento ma nessun utente lo stava aspettando.

scripts è l'array che leggi per l'attribuzione. Ogni voce è uno script eseguito per almeno 5ms. Include sourceURL, sourceFunctionName, invoker, invokerType, duration e forcedStyleAndLayoutDuration. L'ultimo è critico: ti dice se lo script ha innescato un layout sincrono, che è la causa principale della maggior parte dei colli di bottiglia del processing time che vedo negli audit.

Il campo invokerType su ogni script ti dice come è stato chiamato. L'elenco completo è event-listener (clic, scorrimenti, keydown), user-callback (setTimeout, setInterval, requestAnimationFrame), resolve-promise e reject-promise (gestori then/catch), classic-script, e module-script. Per l'INP, quelli che leggi per primi sono event-listener per il processing time, e user-callback, classic-script o module-script per l'input delay. I gestori delle Promise raramente sono in cima alla lista.

Mappa questi campi alle tre fasi dell'INP e il quadro è chiaro. L'input delay si manifesta come script in esecuzione prima di firstUIEventTimestamp. Il processing time è rappresentato da script event-listener in esecuzione dopo l'evento UI. Il presentation delay è tutto ciò che è compreso tra renderStart e la fine del frame.

Catturare LoAF in produzione con web-vitals.js

Non è necessario creare un observer LoAF da zero. La build di attribuzione di web-vitals.js lo fa per te e correla le voci LoAF alle misurazioni effettive dell'INP. Questa è la versione che installo sui siti dei clienti.

import { onINP } from 'web-vitals/attribution';

onINP((metric) => {
    const a = metric.attribution;

    const payload = {
        value: metric.value,
        rating: metric.rating,
        url: location.pathname,
        loadState: a.loadState,
        interactionTarget: a.interactionTarget,
        interactionType: a.interactionType,
        inputDelay: a.inputDelay,
        processingDuration: a.processingDuration,
        presentationDelay: a.presentationDelay,
        longestScriptDuration: a.longestScript?.entry?.duration,
        longestScriptInvoker: a.longestScript?.entry?.invoker,
        longestScriptSource: a.longestScript?.entry?.sourceURL,
        longestScriptSubpart: a.longestScript?.subpart,
        longestScriptIntersecting: a.longestScript?.intersectingDuration,
    };

    navigator.sendBeacon('/rum', JSON.stringify(payload));
});

Quel payload è abbastanza piccolo da poter essere inviato senza campionamento. Tre campi svolgono la maggior parte del lavoro diagnostico: longestScriptInvoker, longestScriptSource e longestScriptSubpart. I primi due ti dicono cosa è stato eseguito. Il terzo ti dice in quale fase dell'INP è atterrato.

Se desideri un'attribuzione completa anziché solo lo script più lungo, invia anche a.longAnimationFrameEntries. Fai attenzione che una singola misurazione INP può includere più LoAF e un LoAF pesante può includere dieci o più voci di script. Inviare l'intero array a ogni visualizzazione di pagina è oneroso. Di solito trasmetto l'intero array solo quando metric.rating !== 'good'.

Un dettaglio importante. L'array longAnimationFrameEntries è vuoto quando LoAF e l'interazione candidata INP non si sovrappongono, o quando il browser non supporta affatto LoAF. Esegui sempre il feature-detect prima di trattare un array vuoto come un segnale pulito.

const loafSupported = PerformanceObserver.supportedEntryTypes?.includes('long-animation-frame');

Considerazioni sulla privacy. I valori sourceURL degli script possono contenere parametri di query e percorsi completi. Per il lavoro sui clienti rimuovo tutto tranne l'origine e il pathname prima dell'invio a RUM. I nomi dei file di bundle con hash sono utili per il debug all'interno di un deploy ma inutili per il raggruppamento tra vari deploy, quindi elimino anche il segmento con hash.

Modelli di attribuzione da 925.000 URL

CoreDash cattura l'attribuzione LoAF per ogni misurazione INP sui siti che monitoriamo. Nel nostro set di dati di 925.000 URL, la stessa manciata di categorie di script emerge ripetutamente come causa dominante dei LoAF bloccanti.

CoreDash dashboard showing LoAF attribution grouped by script origin

Sui siti di e-commerce, le origini bloccanti più frequenti sono costanti. I tag manager che eseguono tag HTML personalizzati sincroni sono al primo posto. Le piattaforme di gestione del consenso che effettuano l'inserimento di script in late-binding sono al secondo. L'orchestrazione degli script di annunci e bid è al terzo. Il quarto posto è occupato dai widget di raccomandazione dei prodotti che effettuano un'idratazione pesante lato client. Il quinto sono gli script di replay della sessione che tracciano ogni interazione.

Ciò che sorprende i clienti è la quota di forcedStyleAndLayoutDuration. In un tipico frame con INP scarso, dal 30 al 60% della durata dello script più lungo è dovuto allo script che innesca un layout sincrono. Lo script non è lento perché il JavaScript è pesante. È lento perché JavaScript legge il layout (offsetHeight, getBoundingClientRect) all'interno di un ciclo che scrive anche nel DOM. Layout thrashing. È lo stesso problema che gli sviluppatori scrivono da quindici anni, e rimane ancora il maggior responsabile individuale dell'aumento del processing time sui siti che controllo.

Nel nostro set di dati, i richiami event-listener rappresentano circa la metà degli script più lunghi che intersecano l'INP. user-callback (setTimeout, setInterval, requestAnimationFrame) copre circa un quarto. L'esecuzione di primo livello di classic-script rappresenta la maggior parte di ciò che resta. I risolutori di Promise sono raramente gli script più lunghi, il che contraddice l'assunto comune secondo cui "il codice asincrono è la causa" dell'INP.

CoreDash chart showing invokerType distribution for INP-blocking LoAF scripts

Le cinque forme LoAF che vedo più spesso

Dopo un numero sufficiente di audit, le voci LoAF iniziano a sembrare familiari. La maggior parte delle interazioni fallite corrisponde a una di queste cinque forme. Ognuna ha una correzione diversa.

Il listener di eventi di terze parti

Lo script più lungo ha invokerType: "event-listener" e un sourceURL da un'origine di terze parti. La durata dello script copre la maggior parte del frame e forcedStyleAndLayoutDuration è bassa. Questo è un tag di un fornitore che ascolta i tuoi clic e fa troppo lavoro nell'handler. La correzione è raramente "rimuovere il tag". La correzione consiste nel posticipare il lavoro svolto dal tag. In particolare per i push del dataLayer c'è un modello che ti fa recuperare dai 20 ai 100ms senza interrompere gli analytics, che ho illustrato in Scheduling dataLayer Events to optimize the INP.

Layout thrashing all'interno del tuo stesso handler

Lo script più lungo ha invokerType: "event-listener", il sourceURL è il tuo stesso bundle, e forcedStyleAndLayoutDuration è più del 30% della durata dello script. Lo script sta leggendo il layout in un ciclo. La soluzione è raggruppare in batch le letture prima delle scritture, oppure spostare il lavoro in requestAnimationFrame e usare valori nella cache.

Lo vedo più spesso nelle pagine dei filtri prodotto dei siti di e-commerce. L'handler del filtro aggiorna il conteggio dei prodotti visibili, poi legge la nuova altezza dell'elenco dei risultati per riposizionare un pulsante del filtro sticky, poi aggiorna una proprietà personalizzata CSS in base a quell'altezza, e infine legge di nuovo l'altezza. Quattro layout forzati in un solo handler. LoAF inserisce questi 80-150ms di forcedStyleAndLayoutDuration in una singola voce di script e la correzione diventa ovvia. Leggere una volta, scrivere una volta, uscire.

Cascata di idratazione alla prima interazione

Il LoAF scatta mentre loadState è ancora "loading" o "dom-interactive". firstUIEventTimestamp si trova all'interno di un frame dove scripts contiene callback di idratazione del framework. L'utente ha cliccato prima che la pagina fosse idratata. La soluzione non è rendere l'idratazione più veloce, sebbene questo aiuti. La soluzione è far funzionare gli elementi interattivi senza idratazione: link nativi e invii di form, widget di disclosure nativi, finestre di dialogo native. Progressive enhancement.

Presentation delay causato da un DOM pesante

Questo aspetto è diverso nei dati. blockingDuration è basso. L'array scripts è corto. Ma il divario tra styleAndLayoutStart e la fine del frame è di oltre 100ms. Nessuna correzione JavaScript aiuterà. Il renderer è intasato dal ricalcolo dello stile su migliaia di nodi. Usa content-visibility: auto sulle sezioni non visibili sullo schermo, contain: layout style sui componenti pesanti e riduci le dimensioni dello scope dello stile che deve ricalcolare ad ogni interazione.

La tempesta di rAF

Più LoAF si concatenano, ciascuno con uno script user-callback invocato da Window.requestAnimationFrame. La pagina sta eseguendo un'animazione in JavaScript che compete con l'utente. Il comportamento di scroll guidato da JavaScript è la versione più comune. Ho visto siti aggiungere 200ms di presentation delay a ogni clic su ogni singola pagina a causa di un polyfill di smooth-scroll di cui nessuno si ricorda più. Ho trattato il caso dello scroll in Improve the INP by ditching JavaScript scrolling. Stessa logica per qualsiasi animazione in JavaScript: passa ai CSS o alla Web Animations API e i LoAF scompaiono.

Che aspetto ha un payload LoAF sul campo

Ecco una voce LoAF reale tratta dall'audit di una pagina di checkout. Ho reso anonimi gli URL ma la struttura e le tempistiche sono intatte.

{
    "name": "long-animation-frame",
    "entryType": "long-animation-frame",
    "startTime": 5902,
    "duration": 320,
    "blockingDuration": 268,
    "renderStart": 6170,
    "styleAndLayoutStart": 6172,
    "firstUIEventTimestamp": 5913,
    "scripts": [
        {
            "name": "script",
            "entryType": "script",
            "invoker": "BUTTON#checkout-submit.onclick",
            "invokerType": "event-listener",
            "sourceURL": "https://cdn.example.com/app.HASH.js",
            "sourceFunctionName": "handleCheckoutSubmit",
            "duration": 248,
            "forcedStyleAndLayoutDuration": 142,
            "executionStart": 5914,
            "startTime": 5914
        }
    ]
}

Leggilo in questo modo. Il frame è iniziato a 5902ms dopo il caricamento della pagina. A 5913ms l'utente ha cliccato sul pulsante per confermare il checkout. Il gestore del clic è iniziato a 5914ms ed è durato 248ms. Di questi 248ms, 142ms sono stati di layout sincrono forzato. Il renderer non è potuto partire fino a 6170ms, più di un quarto di secondo dopo il clic. L'INP per questa interazione si aggira sui 270ms, nell'area "needs improvement".

La soluzione non è "rendere handleCheckoutSubmit più veloce". La soluzione è "smettere di forzare il layout all'interno di handleCheckoutSubmit". 142ms su 248ms non sono JavaScript. Si tratta di leggere i valori di layout che le scritture di JavaScript hanno invalidato. Nel caso di utilizzo reale da cui è tratto l'esempio, mettere in cache i valori di lettura una volta prima del ciclo di scrittura ha fatto scendere la durata dello script da 248ms a 110ms. L'INP della pagina di checkout è passato dal p75 di 270ms a meno di 200ms. La pagina ha superato il test.

Questa è il tipo di correzione che non compare in nessuno strumento di laboratorio. Lighthouse non può dirti nulla su forcedStyleAndLayoutDuration perché Lighthouse non interagisce con la pagina. LoAF sul campo è l'unico modo per vederlo.

DevTools Console output showing the LoAF summary table printed by the webperf-snippets script

Pilotare un agente IA con i dati LoAF

Il motivo per cui LoAF è importante per il debug assistito dall'IA è che fornisce a un agente qualcosa di concreto su cui ragionare. Un agente senza dati sul campo può indovinare le cause dell'INP dal tuo codice. Un agente con le voci LoAF dalle sessioni utente reali può nominare lo script, la funzione e la fase. La diagnosi non è più un'ipotesi.

Il workflow che utilizzo con Claude Code: estrarre la pagina con l'INP peggiore da CoreDash tramite il server MCP, allegare le voci LoAF per le interazioni più lente di quell'URL, e chiedere all'agente di identificare a quale delle cinque forme precedenti corrisponde ogni voce. L'agente classifica ogni LoAF, indica la funzione nel codice e propone una correzione. Revisiono la correzione. La applico. CoreDash misura se l'INP p75 si muove.

Questo funziona perché i dati LoAF sono strutturati e piccoli. Un singolo payload LoAF JSON si adatta in una frazione della finestra di contesto di un agente. Una manciata di LoAF rappresentativi per una singola pagina è sufficiente a guidare una correzione. Ho scritto del modello più ampio in Fix INP with an AI agent: the metric lab tools cannot measure e AI Agent Core Web Vitals: why field data changes everything.

La realtà cross-browser

LoAF è un'esclusiva Chromium. Safari non lo implementa. Firefox non lo implementa. Con l'INP che adesso è presente su Safari 26.2 e Firefox 144, ti ritrovi con un gap di misurazione. I tuoi dati INP di RUM sono cross-browser. I tuoi dati di attribuzione sono di Chrome ed Edge.

Vedo clienti esitare nell'adottare LoAF per questo motivo. Vogliono aspettare che Safari lo implementi. Questa è una decisione sbagliata. Chrome è dove la maggior parte dei bug di performance viene prima riprodotta e la maggior parte delle correzioni di performance viene prima convalidata. La natura del problema (handler di eventi lungo, layout thrashing, cascata di idratazione) non cambia tra browser. Varia solo la lente diagnostica. Le correzioni che rilasci basate sui dati LoAF di Chromium migliorano l'INP anche per gli utenti Safari e Firefox.

Se vuoi un qualche tipo di attribuzione in Safari oggi, EventTiming è quello di cui disponi. Ti dice quale evento è stato lento ma non quale script. Questo è sufficiente per sapere dove guardare ma non cosa correggere. Per l'attribuzione in Safari, il profiling in laboratorio con Safari Web Inspector è un pratico fallback.

Cosa LoAF non può ancora dirti

Vale la pena conoscere tre punti ciechi perché ti coglieranno alla sprovvista a un certo punto.

Il primo sono gli iframe cross-origin. LoAF attribuisce gli script in esecuzione sul thread principale delle finestre che condividono l'origine del documento. Uno script bloccante all'interno di un iframe di terze parti si mostra come un'attribuzione opaca: un frame lungo e un array scripts vuoto. La correzione è la stessa per qualsiasi problema di performance degli iframe di terze parti: ritardare il caricamento dell'iframe, assegnargli dimensioni esplicite, e usare loading="lazy" se è below the fold.

I worker sono il secondo punto cieco. I web worker non compaiono nelle voci LoAF perché LoAF è una API per il thread principale. Se il tuo INP è scarso perché un worker sta inviando pesanti messaggi indietro al thread principale, LoAF ti mostra il gestore dei messaggi sul thread principale ma non il lavoro del worker che lo ha innescato. Il controllo incrociato viene eseguito manualmente tramite indicatori di performance all'interno del worker.

Ed infine, il ritardo del compositor della GPU. La fine di una voce LoAF si ha quando il renderer completa il suo lavoro sul thread principale. Il momento in cui i pixel sono effettivamente sullo schermo avviene dopo, nel compositor e nella GPU. Sui dispositivi Android di fascia bassa il ritardo del compositor può aggiungere da 30 a 100ms che LoAF non vede. Il campo presentationTime sulle voci LoAF (attualmente dietro il flag sperimentale PaintTimingMixin) ha lo scopo di risolvere questo problema ma non è ancora stabile nelle varie versioni di Chrome.

Nessuno di questi fattori è fatale. Significano solo che LoAF non è tutta la storia. Per l'80% dei problemi di INP sui siti reali dei clienti, è sufficiente.

Da dove iniziare

Se non hai mai esaminato i dati LoAF prima, inizia sul campo, non in laboratorio. Installa la build di attribuzione di web-vitals.js, trasmetti lo script più lungo per ogni misurazione dell'INP e guarda i primi dieci valori di longestScriptSource nel corso di una settimana di traffico. Qualunque cosa si trovi in cima è la tua prima correzione, indipendentemente da ciò che dice il tuo report in Lighthouse.

Se desideri la vista del laboratorio, il pannello Performance dei Chrome DevTools non mostra LoAF in modo nativo ma i dati sono disponibili tramite PerformanceObserver. L'approccio della traccia personalizzata che utilizza performance.measure con il campo di dettaglio devtools fa emergere le voci LoAF all'interno del pannello. Il progetto webperf-snippets ha uno snippet per console che stampa una tabella riassuntiva delle voci LoAF insieme alle interazioni, ed è ciò che eseguo per primo di solito quando eseguo il debug di una pagina specifica.

Esplora il resto della serie sull'INP

Il playbook LoAF si inserisce all'interno di un workflow più ampio. Per approfondire ogni fase o passaggio:

Fonti: Chrome for Developers, Long Animation Frames API, MDN, Long animation frame timing, W3C Long Animation Frames specification, Libreria GoogleChrome/web-vitals.

About the author

Arjen Karel is a web performance consultant and the creator of CoreDash, a Real User Monitoring platform that tracks Core Web Vitals data across hundreds of sites. He also built the Core Web Vitals Visualizer Chrome extension. He has helped clients achieve passing Core Web Vitals scores on over 925,000 mobile URLs.

Porto i siti a passare i Core Web Vitals.

Oltre 500K pagine per grandi editori e piattaforme e-commerce in Europa. Scrivo io il fix e lo verifico con dati reali dal campo.

Come lavoro

Risposte alle vostre domande sulla Long Animation Frames API

Supporto del browser e cosa sostituisce LoAF

La Long Animation Frames API è supportata in tutti i browser?

No. LoAF è un'esclusiva Chromium. Chrome, Edge, Opera e Brave ce l'hanno. Safari e Firefox no. Esegui sempre il feature-detect con PerformanceObserver.supportedEntryTypes?.includes('long-animation-frame') prima di farvi affidamento.

LoAF sostituisce la Long Tasks API?

Per il debug dell'INP, sì. Long Tasks ti dà la durata di blocco del thread principale senza l'attribuzione dello script. LoAF ti fornisce lo stesso segnale di blocco, più lo script effettivo che è stato eseguito. Non c'è alcun motivo per usare Long Tasks per i nuovi lavori sull'INP. Il Total Blocking Time potrebbe essere ridefinito in futuro in termini di blockingDuration del LoAF.

Leggere i dati LoAF

Come faccio a correlare una voce LoAF a una specifica misurazione INP?

La build di attribuzione di web-vitals.js lo fa per te tramite attribution.longAnimationFrameEntries. Trova le voci LoAF la cui finestra temporale si sovrappone all'interazione candidata dell'INP. Se stai sviluppando il tuo observer, confronta entry.startTime e entry.startTime + entry.duration col tempo di interazione, e includi qualsiasi LoAF che la cui finestra temporale si intersechi.

Cosa significa in realtà forcedStyleAndLayoutDuration?

È il tempo impiegato dallo script per innescare un layout sincrono. Quando il tuo JavaScript legge offsetHeight, getBoundingClientRect, o qualsiasi altra proprietà che richieda un layout aggiornato dopo una modifica al DOM, il browser deve svolgere il lavoro di layout in modo sincrono all'interno dello script. Tale lavoro viene riattribuito allo script. Una forcedStyleAndLayoutDuration alta è quasi sempre sinonimo di layout thrashing.

Perché l'array degli scripts è vuoto per alcune voci LoAF?

Tre motivi. In primo luogo, la voce contiene unicamente script eseguiti per almeno 5ms. Un frame pieno di script corti al di sotto di questa soglia non mostrerà alcuna attribuzione. In secondo luogo, gli script eseguiti all'interno di iframe cross-origin, web worker, service worker o estensioni del browser non vengono attribuiti poiché LoAF vede solo il thread principale della stessa origine. Terzo, il frame potrebbe non avere alcun lavoro di script, solo stile e layout, il che è un frame di presentation delay.

Oltre l'INP

LoAF può aiutare con l'LCP e non solo con l'INP?

Sì, indirettamente. Un LoAF eseguito prima del rendering dell'elemento LCP è un lungo script che ritarda il rendering dell'LCP. Filtra i LoAF tra quelli la cui ora di fine precede il timestamp dell'LCP: lo script più lungo lì presente è il maggior responsabile del ritardo di rendering dell'LCP. Gli stessi dati di attribuzione, applicati a una metrica differente.