Long Animation Frames API: Depurar INP em Produção
Um manual de produção para encontrar o script que quebrou sua interação

Durante anos, a resposta para "o que está causando meu INP" foi frequentemente recebida com um olhar vazio. A Long Tasks API dizia que a thread principal estava bloqueada por mais de 50ms. Ela não dizia qual script. Ela não dizia qual linha. Tudo o que tínhamos era um número em ms.
A Long Animation Frames API corrige isso. Ela foi lançada nos navegadores Chromium a partir do Chrome 123 e fornece a anatomia completa de um frame: duração total, duração do bloqueio, o momento em que o renderizador começou, o momento em que o estilo e o layout começaram, e um array de cada script que rodou no frame por mais de 5ms. Cada entrada de script inclui a URL de origem, o nome da função, a posição do caractere no arquivo e o tipo de callback que a invocou.
Estes são os dados de que a depuração de INP sempre precisou. O resto deste artigo é sobre como usá-los em um site de produção real sem piorar as coisas.
Esta página faz parte da nossa série de Interaction to Next Paint (INP). O manual de LoAF abaixo é o que eu executo após diagnosticar a métrica. Se você é novo no INP, comece com os guias de três fases para input delay, processing time e presentation delay, e o fluxo de trabalho mais amplo de encontrar e consertar o INP.
Por que as Long Tasks não eram suficientes
Uma long task é disparada quando uma única tarefa na thread principal roda por mais de 50ms. Isso é útil para detectar os bloqueadores óbvios. É inútil para o INP porque a maioria das interações lentas não é uma única long task. Elas são uma sequência de tarefas médias mais uma renderização longa.
Uma interação pode falhar no INP sem nunca acionar uma long task. Um manipulador de eventos de 40ms seguido por um recalculo de estilo de 70ms soma 110ms de presentation delay. A Long Tasks API não reporta nada. O usuário sente a lentidão.
A outra falha das Long Tasks é a atribuição. Você obtém um tipo de contêiner, um nome de contêiner e um campo de nome informando se a long task foi "self", "same-origin-ancestor", "same-origin-descendant", "unknown", ou uma de algumas variantes de origem cruzada. Você não obtém o script que rodou. Portanto, mesmo quando você pega uma long task, você ainda precisa abrir o DevTools e regravar a interação para encontrar a causa. Isso funciona em um laboratório. Não funciona para o INP em usuários reais que você não pode reproduzir.
O LoAF substitui ambas as limitações. Ele captura o frame inteiro, não uma única tarefa. E diz exatamente qual script rodou, por URL e nome da função.
Os sete campos que importam
Uma entrada do LoAF tem mais campos do que você precisa. Estes sete são os que eu leio primeiro ao depurar uma interação.
duration é o comprimento total do frame em milissegundos. Uma entrada do LoAF só é criada quando isso excede 50ms. Esse número por si só diz se o frame é um problema, mas não o porquê.
blockingDuration é mais útil. Ele soma as partes do frame que excederam o limite da long task, subtraindo 50ms de cada uma. Um frame com duration: 320 e blockingDuration: 0 é predominantemente trabalho de renderização. Um frame com duration: 320 e blockingDuration: 270 é um script longo. O primeiro precisa de uma correção de renderização. O segundo precisa de uma mudança de código.
renderStart é o timestamp em que o navegador começou a fazer o trabalho de renderização para o frame. Tudo entre startTime e renderStart é execução de script. Tudo depois é estilo, layout e pintura (paint). Este é o limite mais útil para a depuração de INP porque informa se o gargalo é JavaScript ou renderização.
styleAndLayoutStart é o timestamp em que a renderização passou da execução de callbacks de frame de animação para o recálculo de estilo e layout real. A lacuna entre renderStart e styleAndLayoutStart são seus callbacks do requestAnimationFrame. A lacuna entre styleAndLayoutStart e o final do frame é o trabalho de estilo e layout do lado do navegador.
firstUIEventTimestamp é quando uma entrada do usuário chegou durante o frame. Se esse valor for diferente de zero, o frame contém uma interação. É assim que o web-vitals.js correlaciona entradas do LoAF com medições do INP. Nenhum firstUIEventTimestamp significa que o frame está lento, mas nenhum usuário estava esperando por ele.
scripts é o array que você lê para atribuição. Cada entrada é um script que rodou por pelo menos 5ms. Ele inclui sourceURL, sourceFunctionName, invoker, invokerType, duration e forcedStyleAndLayoutDuration. O último é crítico: ele diz se o script acionou layout síncrono, que é a causa raiz da maioria dos gargalos de processing time que eu vejo em auditorias.
O campo invokerType em cada script informa como ele foi chamado. A lista completa é event-listener (cliques, rolagens, pressionamentos de tecla), user-callback (setTimeout, setInterval, requestAnimationFrame), resolve-promise e reject-promise (manipuladores then/catch), classic-script e module-script. Para o INP, os que você lê primeiro são event-listener para processing time, e user-callback, classic-script ou module-script para input delay. Manipuladores de promise raramente lideram a lista.
Mapeie esses campos para as três fases do INP e a imagem ficará clara. Input delay aparece como scripts executados antes do firstUIEventTimestamp. Processing time são scripts de event-listener executados após o evento de UI. Presentation delay é tudo entre renderStart e o final do frame.
Capturando LoAF em produção com web-vitals.js
Você não precisa construir um observador de LoAF do zero. A build de atribuição do web-vitals.js faz isso por você e correlaciona as entradas do LoAF com medições reais do INP. Esta é a versão que instalo nos sites de clientes.
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));
}); Esse payload é pequeno o suficiente para ser enviado sem amostragem. Três campos fazem a maior parte do trabalho de diagnóstico: longestScriptInvoker, longestScriptSource e longestScriptSubpart. Os dois primeiros informam o que foi executado. O terceiro informa em qual fase do INP ele ocorreu.
Se você deseja a atribuição completa em vez de apenas o script mais longo, envie a.longAnimationFrameEntries também. Esteja ciente de que uma medição do INP pode incluir vários LoAFs e um LoAF pesado pode incluir dez ou mais entradas de script. Enviar o array completo em cada visualização de página é custoso. Eu geralmente envio o array completo apenas quando metric.rating !== 'good'.
Um detalhe importante. O array longAnimationFrameEntries fica vazio quando o LoAF e a interação candidata do INP não se sobrepõem, ou quando o navegador não suporta LoAF de forma alguma. Sempre detecte a funcionalidade (feature-detect) antes de tratar um array vazio como um sinal limpo.
const loafSupported = PerformanceObserver.supportedEntryTypes?.includes('long-animation-frame'); A consideração de privacidade. Valores de sourceURL de scripts podem conter parâmetros de consulta e caminhos completos. Para o trabalho do cliente, eu removo tudo, exceto a origem e o pathname, antes de enviar para o RUM. Nomes de arquivos de bundle com hash são úteis para depuração dentro de um deploy, mas inúteis para agrupamento através de deploys, então eu também removo o segmento de hash.
Padrões de atribuição de 925.000 URLs
O CoreDash captura a atribuição de LoAF para todas as medições do INP nos sites que monitoramos. Em nosso conjunto de dados de 925.000 URLs, o mesmo punhado de categorias de script aparece como a causa dominante do bloqueio de LoAFs repetidas vezes.
Em sites de e-commerce, as principais origens de bloqueio são consistentes. Gerenciadores de tags executando tags HTML personalizadas síncronas estão em primeiro lugar. Plataformas de gerenciamento de consentimento fazendo injeção de script de ligação tardia (late-binding) estão em segundo. A orquestração de scripts de anúncios e lances está em terceiro. O quarto lugar são os widgets de recomendação de produtos que hidratam muito o lado do cliente. O quinto são scripts de repetição de sessão que instrumentam cada interação.
O que surpreende os clientes é a participação de forcedStyleAndLayoutDuration. Em um frame de INP pobre típico, de 30 a 60% da duração do script mais longo é o script acionando layout síncrono. O script não é lento porque o JavaScript é pesado. É lento porque o JavaScript lê o layout (offsetHeight, getBoundingClientRect) dentro de um loop que também grava no DOM. Layout thrashing. O mesmo problema que os desenvolvedores vêm escrevendo há quinze anos, ainda o maior contribuinte individual para o processing time nos sites que audito.
Em todo o nosso conjunto de dados, os invokers event-listener respondem por cerca de metade dos scripts mais longos que se cruzam com o INP. user-callback (setTimeout, setInterval, requestAnimationFrame) é responsável por cerca de um quarto. A execução de nível superior do classic-script é responsável por grande parte do que resta. Os resolutores de promise raramente são o script mais longo, o que contradiz a suposição comum de que "o código assíncrono é a causa" do INP.
As cinco formas de LoAF que vejo com mais frequência
Após auditorias suficientes, as entradas do LoAF começam a parecer familiares. A maioria das interações que falham corresponde a uma de cinco formas. Cada uma tem uma correção diferente.
O event listener de terceiros
O script mais longo tem invokerType: "event-listener" e um sourceURL de uma origem de terceiros. A duração do script é a maior parte do frame e forcedStyleAndLayoutDuration é baixa. Esta é uma tag de fornecedor ouvindo seus cliques e fazendo muito trabalho no manipulador. A correção raramente é "remover a tag". A correção é adiar o trabalho que a tag faz. Para pushes no dataLayer especificamente, há um padrão que recupera de 20 a 100ms sem quebrar a análise, o qual abordei em Agendando eventos do dataLayer para otimizar o INP.
Layout thrash dentro do seu próprio manipulador
O script mais longo tem invokerType: "event-listener", o sourceURL é o seu próprio bundle e a forcedStyleAndLayoutDuration é mais de 30% da duração do script. O script está lendo o layout em um loop. A correção é agrupar as leituras antes das gravações, ou mover o trabalho para o requestAnimationFrame e usar valores em cache.
Eu vejo isso com mais frequência em páginas de filtro de produtos em sites de e-commerce. O manipulador de filtro atualiza a contagem de produtos visíveis, então lê a nova altura da lista de resultados para reposicionar um botão de filtro aderente (sticky), em seguida, atualiza uma propriedade personalizada CSS com base nessa altura e, em seguida, lê a altura novamente. Quatro layouts forçados em um manipulador. O LoAF coloca esses 80 a 150ms de forcedStyleAndLayoutDuration em uma única entrada de script e a correção torna-se óbvia. Leia uma vez, grave uma vez, saia.
Cascata de hidratação na primeira interação
O LoAF dispara enquanto o loadState ainda é "loading" ou "dom-interactive". O firstUIEventTimestamp fica dentro de um frame onde scripts contém callbacks de hidratação do framework. O usuário clicou antes da página ser hidratada. A correção não é tornar a hidratação mais rápida, embora isso ajude. A correção é fazer com que os elementos interativos funcionem sem hidratação: links nativos e submissões de formulários, widgets de divulgação (disclosure) nativos, diálogos nativos. Aprimoramento progressivo.
Presentation delay de um DOM pesado
Este aqui parece diferente nos dados. A blockingDuration é baixa. O array scripts é curto. Mas a lacuna entre styleAndLayoutStart e o final do frame é de mais de 100ms. Nenhuma correção no JavaScript ajudará. O renderizador está engasgando no recálculo de estilo em milhares de nós. Use content-visibility: auto em seções fora da tela, contain: layout style em componentes pesados e reduza o tamanho do escopo de estilo que precisa ser recalculado por interação.
A tempestade de rAF
Vários LoAFs se encadeiam, cada um com um script de user-callback invocado pelo Window.requestAnimationFrame. A página está executando uma animação em JavaScript que compete com o usuário. O comportamento de rolagem acionado por JavaScript é a versão mais comum. Eu já vi sites adicionarem 200ms de presentation delay a cada clique em cada página por causa de um polyfill de rolagem suave (smooth-scroll) que ninguém lembra de ter lançado. Eu abordei o caso de rolagem em Melhore o INP abandonando a rolagem com JavaScript. A mesma lógica para qualquer animação em JavaScript: mude para CSS ou para a Web Animations API e os LoAFs desaparecem.
Como é um payload do LoAF em campo
Aqui está uma entrada real de LoAF de uma auditoria em uma página de checkout. Eu tornei as URLs anônimas, mas a estrutura e as temporizações estão intactas.
{
"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
}
]
} Leia da seguinte maneira. O frame começou aos 5902ms após o carregamento da página. Aos 5913ms, o usuário clicou no botão de envio do checkout. O manipulador de cliques começou aos 5914ms e rodou por 248ms. Desses 248ms, 142ms foram de layout síncrono forçado. O renderizador só pôde começar aos 6170ms, mais de um quarto de segundo após o clique. O INP para esta interação é de cerca de 270ms, na zona "precisa melhorar".
A correção não é "tornar o handleCheckoutSubmit mais rápido". A correção é "parar de forçar layout dentro do handleCheckoutSubmit". 142ms de 248ms não é o JavaScript. Está lendo valores de layout que as gravações do JavaScript invalidaram. No engajamento real de onde isso veio, o armazenamento em cache (caching) dos valores lidos uma vez antes do loop de gravação derrubou a duração do script de 248ms para 110ms. O INP na página de checkout passou do p75 de 270ms para menos de 200ms. A página passou.
Esse é o tipo de correção que não aparece em nenhuma ferramenta de laboratório. O Lighthouse não pode falar sobre a forcedStyleAndLayoutDuration porque o Lighthouse não interage com a página. O LoAF no campo (RUM) é a única maneira de ver isso.
Conduzindo um agente de IA com dados do LoAF
A razão pela qual o LoAF é importante para a depuração assistida por IA é que ele dá a um agente algo concreto sobre o que raciocinar. Um agente sem dados de campo pode adivinhar as causas do INP a partir do seu código. Um agente com entradas do LoAF de sessões reais de usuários pode nomear o script, a função e a fase. O diagnóstico deixa de ser um palpite.
O fluxo de trabalho que uso com o Claude Code: puxo a pior página de INP do CoreDash através do servidor MCP, anexo as entradas do LoAF para as interações mais lentas daquela URL, e peço ao agente para identificar a qual das cinco formas acima cada entrada corresponde. O agente classifica cada LoAF, aponta a função na base de código e propõe uma correção. Eu reviso a correção. Eu a aplico. O CoreDash mede se o INP p75 muda.
Isso funciona porque os dados do LoAF são estruturados e pequenos. Um único payload JSON de LoAF cabe em uma fração da janela de contexto de um agente. Um punhado de LoAFs representativos para uma única página é suficiente para guiar uma correção. Escrevi sobre o padrão mais amplo em Corrigindo o INP com um agente de IA: a métrica que as ferramentas de laboratório não conseguem medir e Agente de IA e Core Web Vitals: por que os dados de campo mudam tudo.
A realidade cross-browser
O LoAF é exclusivo do Chromium. O Safari não o implementa. O Firefox não o implementa. Com o próprio INP agora sendo lançado no Safari 26.2 e no Firefox 144, você tem uma lacuna de medição. Seus dados de INP de RUM são de vários navegadores. Seus dados de atribuição são do Chrome e do Edge.
Eu vejo os clientes hesitarem em adotar o LoAF por causa disso. Eles querem esperar até que o Safari o lance. Essa é a decisão errada. O Chrome é onde a maioria dos bugs de desempenho é reproduzida pela primeira vez e a maioria das correções de desempenho é validada primeiro. O formato do problema (manipulador de eventos longo, layout thrash, cascata de hidratação) não muda entre os navegadores. Apenas a lente de diagnóstico difere. As correções que você lança com base nos dados do LoAF do Chromium melhoram o INP para usuários do Safari e do Firefox também.
Se você quiser algum tipo de atribuição no Safari hoje, o EventTiming é o que você tem. Ele diz qual evento foi lento, mas não qual script. Isso é suficiente para saber onde procurar, mas não o que corrigir. Para atribuição no Safari, o perfil de laboratório no Safari Web Inspector é a alternativa prática.
O que o LoAF ainda não pode lhe dizer
Vale a pena conhecer três pontos cegos porque eles vão te pegar de surpresa em algum momento.
O primeiro são iframes de origem cruzada. O LoAF atribui apenas scripts executados na thread principal de janelas que compartilham a origem do documento. Um script de bloqueio dentro de um iframe de terceiros aparece como uma atribuição opaca: um frame longo e um array de scripts vazio. A correção é a mesma que para qualquer problema de desempenho de iframe de terceiros: atrase o carregamento do iframe, dê-lhe dimensões explícitas e use loading="lazy" se estiver abaixo da dobra (below the fold).
Os workers são o segundo ponto cego. Os web workers não aparecem nas entradas do LoAF porque o LoAF é uma API da thread principal. Se o seu INP estiver ruim porque um worker está enviando mensagens pesadas de volta à thread principal, o LoAF mostrará o manipulador de mensagens na thread principal, mas não o trabalho do worker que o acionou. Você cruza as referências manualmente usando marcas de performance dentro do worker.
E, finalmente, atraso no compositor da GPU. O fim de uma entrada do LoAF é quando o renderizador termina seu trabalho na thread principal. O momento real dos pixels na tela é posterior, no compositor e na GPU. Em dispositivos Android de baixo custo, o atraso do compositor pode adicionar de 30 a 100ms que o LoAF não enxerga. O campo presentationTime nas entradas do LoAF (atualmente por trás da flag experimental PaintTimingMixin) tem o objetivo de resolver isso, mas ainda não é estável nas versões do Chrome.
Nenhum desses é fatal. Eles apenas significam que o LoAF não é a história toda. Para 80% dos problemas do INP em sites de clientes reais, é o suficiente.
Por onde começar
Se você nunca olhou para dados do LoAF antes, comece no campo, não no laboratório. Instale a build de atribuição do web-vitals.js, envie o script mais longo por medição de INP e observe os dez principais valores de longestScriptSource ao longo de uma semana de tráfego. O que estiver no topo é sua primeira correção, independentemente do que o seu relatório do Lighthouse lhe diga.
Se você quer a visão de laboratório, o painel Performance do Chrome DevTools não mostra o LoAF nativamente, mas os dados estão disponíveis via PerformanceObserver. A abordagem de faixa personalizada usando performance.measure com o campo de detalhes do devtools traz à tona as entradas do LoAF dentro do painel. O projeto webperf-snippets tem um snippet de console que imprime uma tabela de resumo de entradas do LoAF com interações, que é o que eu geralmente executo primeiro ao depurar uma página específica.
Explore o resto da série INP
O manual do LoAF se encaixa no fluxo de trabalho maior. Para ir mais fundo em cada fase ou etapa:
- Encontre e conserte problemas de INP: o fluxo de diagnóstico dos dados do RUM para a interação lenta.
- Input delay: scripts sendo executados antes que o manipulador de eventos possa começar.
- Processing time: o próprio manipulador de eventos.
- Presentation delay: o trabalho de renderização após o retorno do manipulador.
- Hub do INP: o guia completo da métrica.
Fontes: Chrome para Desenvolvedores, API de Long Animation Frames, MDN, Tempo de long animation frame, Especificação de Long Animation Frames do W3C, Biblioteca GoogleChrome/web-vitals.
A performance cai por terra assim que deixas de olhar.
Monto o monitoring, os performance budgets e os processos. É a diferença entre um fix e uma solução a sério.
Falamos?Suas Dúvidas Sobre a Long Animation Frames API Respondidas
Suporte a navegadores e o que o LoAF substitui
A Long Animation Frames API é suportada em todos os navegadores?
Não. O LoAF é exclusivo do Chromium. Chrome, Edge, Opera e Brave a possuem. Safari e Firefox não. Sempre detecte a funcionalidade com PerformanceObserver.supportedEntryTypes?.includes('long-animation-frame') antes de confiar nela.
O LoAF substitui a Long Tasks API?
Para a depuração de INP, sim. Long Tasks fornece a duração do bloqueio da thread principal sem nenhuma atribuição de script. O LoAF fornece o mesmo sinal de bloqueio mais o script real que rodou. Não há motivo para usar Long Tasks para um novo trabalho de INP. Total Blocking Time pode eventualmente ser redefinido em termos de blockingDuration do LoAF.
Lendo os dados do LoAF
Como eu correlaciono uma entrada do LoAF com uma medição de INP específica?
A build de atribuição do web-vitals.js faz isso para você via attribution.longAnimationFrameEntries. Ela encontra as entradas do LoAF cuja janela de tempo se sobrepõe à interação candidata do INP. Se você estiver criando seu próprio observador, compare entry.startTime e entry.startTime + entry.duration com o momento da interação e inclua qualquer LoAF cuja janela se intersete.
O que forcedStyleAndLayoutDuration realmente significa?
É o tempo que o script passou acionando um layout síncrono. Quando seu JavaScript lê offsetHeight, getBoundingClientRect ou qualquer outra propriedade que exija um layout atualizado após uma mutação no DOM, o navegador precisa fazer o trabalho de layout de forma síncrona dentro do script. Esse trabalho é atribuído de volta ao script. Uma forcedStyleAndLayoutDuration alta é quase sempre layout thrashing.
Por que o array scripts está vazio para algumas entradas do LoAF?
Três razões. Primeiro, a entrada contém apenas scripts que rodaram por pelo menos 5ms. Um frame cheio de scripts curtos abaixo desse limite não mostrará atribuição. Segundo, os scripts em execução em iframes de origem cruzada, web workers, service workers ou extensões de navegador não são atribuídos porque o LoAF enxerga apenas a thread principal de mesma origem. Terceiro, o frame pode não ter nenhum trabalho de script, apenas estilo e layout, o que é um frame de presentation delay.
Além do INP
O LoAF pode ajudar com o LCP, e não apenas com o INP?
Sim, indiretamente. Um LoAF que roda antes que o elemento do LCP seja renderizado é um script longo atrasando a renderização do LCP. Filtre os LoAFs para aqueles cujo tempo de término é anterior ao timestamp do LCP e o script mais longo ali será o maior contribuinte para o atraso na renderização do LCP. São os mesmos dados de atribuição, aplicados a uma métrica diferente.