Long Animation Frames API: Depure INP em Produção

Um manual de produção para encontrar o script que quebrou sua interação

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

Por anos, a resposta para "o que está causando meu INP" era 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 vem nos navegadores Chromium desde o Chrome 123 e fornece a anatomia completa de um frame: duração total, duração de bloqueio, o momento em que o renderizador iniciou, o momento em que o estilo e o layout iniciaram, 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.

Esses são os dados de que a depuração de INP sempre precisou. O resto deste artigo é sobre como usá-la em um site real em produção sem piorar as coisas.

Esta página faz parte da nossa série sobre Interaction to Next Paint (INP). O manual do 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 atraso de entrada (input delay), tempo de processamento (processing time) e atraso de apresentação (presentation delay), e o fluxo de trabalho mais amplo de encontrar e corrigir INP.

Por que Long Tasks não era suficiente

Uma long task dispara quando uma única tarefa na thread principal roda por mais de 50ms. Isso é útil para capturar os bloqueadores óbvios. É inútil para o INP porque a maioria das interações lentas não é uma 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 disparar uma long task. Um manipulador de eventos de 40ms seguido por um recalculo de estilo de 70ms soma 110ms de atraso de apresentação. A Long Tasks API não relata nada. O usuário sente a lentidão.

A outra falha do 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 das poucas variantes cross-origin. Você não obtém o script que rodou. Portanto, mesmo quando você captura uma long task, você ainda precisa abrir o DevTools e regravar a interação para encontrar a causa. Isso funciona em 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 ele diz exatamente qual script rodou, pela URL e nome da função.

Os sete campos que importam

Uma entrada de LoAF tem mais campos do que você precisa. Esses sete são os que eu leio primeiro ao depurar uma interação.

duration é o comprimento total do frame em milissegundos. Uma entrada de LoAF só é criada quando isso excede 50ms. Este 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 de long task, subtraindo 50ms de cada uma. Um frame com duration: 320 e blockingDuration: 0 é na sua maioria 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 alteração no código.

renderStart é o timestamp onde 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 depuração de INP porque informa se o gargalo é JavaScript ou renderização.

styleAndLayoutStart é o timestamp onde a renderização passou da execução de callbacks de animation frame para o recalculo de estilo e layout real. A lacuna entre renderStart e styleAndLayoutStart são seus callbacks requestAnimationFrame. A lacuna entre styleAndLayoutStart e o fim do frame é o trabalho de estilo e layout do lado do navegador.

firstUIEventTimestamp é quando uma entrada de usuário chegou durante o frame. Se este valor for diferente de zero, o frame contém uma interação. É assim que o web-vitals.js correlaciona entradas de LoAF com medições de 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. Inclui sourceURL, sourceFunctionName, invoker, invokerType, duration e forcedStyleAndLayoutDuration. O último é fundamental: ele informa se o script disparou um layout síncrono, que é a causa raiz da maioria dos gargalos de tempo de processamento que vejo em auditorias.

O campo invokerType em cada script diz como ele foi chamado. A lista completa é event-listener (cliques, rolagens, keydowns), 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 o tempo de processamento, e user-callback, classic-script, ou module-script para o atraso de entrada (input delay). Manipuladores de promise raramente lideram a lista.

Mapeie esses campos para as três fases do INP e a imagem fica clara. O atraso de entrada aparece como scripts rodando antes de firstUIEventTimestamp. O tempo de processamento são os scripts de event-listener rodando depois do evento de UI. O atraso de apresentação é tudo entre renderStart e o fim do frame.

Capturando LoAF em produção com web-vitals.js

Você não precisa construir um observador LoAF do zero. A build de atribuição do web-vitals.js faz isso por você e correlaciona entradas de LoAF a medições reais de INP. Esta é a versão que eu instalo em 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 dizem o que rodou. O terceiro diz em qual fase do INP ele caiu.

Se você quiser a atribuição completa em vez de apenas o script mais longo, envie a.longAnimationFrameEntries também. Esteja ciente de que uma medição de INP pode incluir múltiplos 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 estará vazio quando o LoAF e a interação candidata ao INP não se sobrepuserem, ou quando o navegador não suportar 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. Os valores de sourceURL do script podem conter parâmetros de consulta e caminhos completos. Para o trabalho do cliente, eu removo tudo, exceto a origem (origin) 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 agrupar através de múltiplos deploys, então eu também removo o segmento do hash.

Padrões de atribuição de 925.000 URLs

O CoreDash captura a atribuição de LoAF para cada medição de INP nos sites que monitoramos. Em nosso conjunto de dados de 925.000 URLs, o mesmo punhado de categorias de scripts aparece como a causa dominante de bloqueios LoAFs repetidamente.

CoreDash dashboard showing LoAF attribution grouped by script origin

Em sites de e-commerce, as principais origens de bloqueio são consistentes. Gerenciadores de tags rodando tags HTML personalizadas de forma síncrona estão em primeiro. 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 é a terceira. O quarto são os widgets de recomendação de produtos que fazem uma grande hidratação no lado do cliente (client-side). O quinto são os scripts de repetição de sessão (session replay) que instrumentam cada interação.

O que surpreende os clientes é a participação de forcedStyleAndLayoutDuration. Em um frame típico de INP ruim, 30 a 60% da duração do script mais longo é o script disparando 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 escreve no DOM. Layout thrashing. O mesmo problema que os desenvolvedores vêm escrevendo por quinze anos, ainda é o maior contribuinte individual para o tempo de processamento nos sites que eu audito.

Em todo o nosso conjunto de dados, os invocadores event-listener respondem por cerca da metade dos scripts mais longos que cruzam o INP. user-callback (setTimeout, setInterval, requestAnimationFrame) responde por cerca de um quarto. A execução de nível superior classic-script responde pela maior parte do que resta. Resolvedores de promise raramente são o script mais longo, o que contradiz a suposição comum de que "código assíncrono é a causa" do INP.

CoreDash chart showing invokerType distribution for INP-blocking LoAF scripts

As cinco formas de LoAF que vejo com mais frequência

O event listener de terceiros

O script mais longo tem invokerType: "event-listener" e uma sourceURL de uma origem de terceiros. A duração do script é a maior parte do frame e forcedStyleAndLayoutDuration é baixo. Esta é uma tag de fornecedor (vendor) 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 envios (pushes) de dataLayer especificamente, há um padrão que devolve de 20 a 100ms sem quebrar o analytics, 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", a sourceURL é o seu próprio bundle e forcedStyleAndLayoutDuration é mais de 30% da duração do script. O script está lendo layout em um loop. A correção é agrupar as leituras (batch) antes das escritas, ou mover o trabalho para o requestAnimationFrame e usar valores em cache.

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, depois lê a nova altura da lista de resultados para reposicionar um botão de filtro fixo (sticky), em seguida atualiza uma propriedade customizada do CSS com base nessa altura e, depois, 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 se torna óbvia. Leia uma vez, escreva uma vez, saia.

Cascata de hidratação na primeira interação

O LoAF dispara enquanto loadState ainda é "loading" ou "dom-interactive". firstUIEventTimestamp fica dentro de um frame onde scripts contém callbacks de hidratação do framework. O usuário clicou antes da página estar hidratada. A correção não é tornar a hidratação mais rápida, embora isso ajude. A correção é fazer os elementos interativos funcionarem sem hidratação: links nativos e envios de formulários (submits), widgets de revelação (disclosure) nativos, diálogos nativos. Melhoria progressiva (progressive enhancement).

Atraso de apresentação por um DOM pesado

Esta parece diferente nos dados. blockingDuration é baixo. O array scripts é curto. Mas a lacuna entre styleAndLayoutStart e o final do frame é de mais de 100ms. Nenhuma correção em JavaScript ajudará. O renderizador está engasgando no recalculo de estilo em milhares de nós. Use content-visibility: auto em seções fora da tela (offscreen), contain: layout style em componentes pesados e reduza o tamanho do escopo de estilo que precisa recalcular por interação.

A tempestade de rAF

Múltiplos LoAFs se encadeiam juntos, cada um com um script de user-callback invocado pelo Window.requestAnimationFrame. A página está rodando uma animação em JavaScript que compete com o usuário. O comportamento de rolagem (scroll) movido a JavaScript é a versão mais comum. Já vi sites adicionarem 200ms de atraso de apresentação em cada clique em todas as páginas por causa de um polyfill de smooth-scroll que ninguém lembra de ter implantado. Abordei o caso de rolagem em Melhore o INP abandonando a rolagem com JavaScript. 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 de página de checkout. Eu anonimizei as URLs, mas a estrutura e os tempos estão intactos.

{
    "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 desta forma. O frame começou aos 5902ms após o carregamento da página. Aos 5913ms, o usuário clicou no botão de envio (submit) do checkout. O manipulador de clique começou aos 5914ms e rodou por 248ms. Desses 248ms, 142ms foram de layout síncrono forçado. O renderizador só conseguiu começar aos 6170ms, mais de um quarto de segundo após o clique. O INP para esta interação é em torno de 270ms, na zona de "precisa de melhoria".

A correção não é "tornar a handleCheckoutSubmit mais rápida". A correção é "parar de forçar o layout dentro da handleCheckoutSubmit". 142ms dos 248ms não é o JavaScript. Está lendo valores de layout que as escritas do JavaScript invalidaram. No engajamento real de onde isso veio, armazenar em cache os valores de leitura uma vez antes do loop de escrita 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 te falar sobre forcedStyleAndLayoutDuration porque o Lighthouse não interage com a página. O LoAF em campo é a única maneira de ver isso.

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

Conduzindo um agente de IA com dados do LoAF

O motivo pelo qual o LoAF importa para depuração assistida por IA é que ele fornece a um agente algo concreto para raciocinar. Um agente sem dados de campo pode adivinhar as causas do INP a partir do seu código. Um agente com entradas de 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: puxar a pior página de INP do CoreDash através do servidor MCP, anexar as entradas de LoAF para as interações mais lentas daquela URL e pedir ao agente para identificar com qual das cinco formas acima cada entrada corresponde. O agente classifica cada LoAF, aponta para a função na base de código (codebase) e propõe uma correção. Eu reviso a correção. Eu aplico. O CoreDash mede se o p75 do INP se move.

Isso funciona porque os dados do LoAF são estruturados e pequenos. Um único payload JSON do LoAF se encaixa em uma fração da janela de contexto de um agente. Um punhado de LoAFs representativos para uma única página é o suficiente para conduzir a uma correção. Escrevi sobre o padrão mais amplo em Corrija o INP com um agente de IA: a métrica que as ferramentas de laboratório não podem medir e Agente de IA e Core Web Vitals: por que os dados de campo mudam tudo.

Realidade entre navegadores (cross-browser)

O LoAF é apenas para Chromium. O Safari não o implementa. O Firefox não o implementa. Com o próprio INP sendo lançado agora no Safari 26.2 e no Firefox 144, você tem uma lacuna de medição. Seus dados de RUM do INP são cross-browser. Seus dados de atribuição são do Chrome e do Edge.

Vejo 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 performance é reproduzida primeiro e a maioria das correções de performance é validada primeiro. A forma do problema (manipulador de evento longo, layout thrash, cascata de hidratação) não muda entre os navegadores. Apenas a lente de diagnóstico difere. Correções que você envia baseadas em dados do LoAF do Chromium melhoram o INP para usuários do Safari e do Firefox também.

Se você quer 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 consertar. Para a atribuição no Safari, o perfilamento em laboratório (lab profiling) no Safari Web Inspector é o fallback prático.

O que o LoAF ainda não pode te dizer

Três pontos cegos valem a pena conhecer, pois vão te pegar de surpresa em algum momento.

O primeiro são os iframes cross-origin. O LoAF apenas atribui scripts rodando 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 scripts vazio. A correção é a mesma para qualquer problema de performance de iframe de terceiros: adie (defer) o carregamento do iframe, dê a ele dimensões explícitas e use loading="lazy" se estiver abaixo da dobra (below the fold).

Workers são o segundo ponto cego. Os web workers não aparecem em entradas de LoAF porque o LoAF é uma API de thread principal (main-thread). Se o seu INP for ruim porque um worker está enviando mensagens pesadas de volta para a thread principal, o LoAF mostra o manipulador de mensagens na thread principal, mas não o trabalho do worker que o disparou. Você faz uma referência cruzada (cross-reference) manualmente usando marcas de performance (performance marks) dentro do worker.

E, finalmente, o atraso do compositor da GPU. O fim de uma entrada de LoAF é quando o renderizador termina seu trabalho na thread principal. O momento real dos pixels na tela é depois, no compositor e na GPU. Em dispositivos Android mais básicos, o atraso do compositor pode adicionar 30 a 100ms que o LoAF não vê. O campo presentationTime em entradas de LoAF (atualmente atrás da flag experimental PaintTimingMixin) foi feito para resolver isso, mas ainda não está estável nas várias versões do Chrome.

Nenhum desses é fatal. Apenas significam que o LoAF não é a história toda. Para 80% dos problemas de INP em sites reais de clientes, ele é suficiente.

Por onde começar

Se você nunca olhou para os dados do LoAF antes, comece no campo, não no laboratório. Instale a build de atribuição do web-vitals.js, envie (beacon) 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 quer que esteja no topo é a sua primeira correção, independentemente do que o seu relatório do Lighthouse diz.

Se você quer a visão de laboratório, o painel de Performance do Chrome DevTools não mostra o LoAF nativamente, mas os dados estão disponíveis via PerformanceObserver. A abordagem de trilha (track) customizada usando performance.measure com o campo de detalhes devtools traz à tona (surfaces) as entradas de LoAF dentro do painel. O projeto webperf-snippets tem um snippet de console que imprime uma tabela de resumo das entradas de LoAF com interações, que é o que eu geralmente rodo primeiro ao depurar uma página específica.

Explore o resto da série sobre INP

O manual do LoAF se encaixa dentro do fluxo de trabalho maior. Para se aprofundar em cada fase ou etapa:

Fontes: Chrome for Developers, Long Animation Frames API, MDN, Long animation frame timing, Especificação W3C Long Animation Frames, Biblioteca 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.

Search Console reclamou do seu site?

Entrego uma lista priorizada de fixes baseada em dados de campo. Não um PDF de 50 páginas.

Solicitar auditoria

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 é apenas para Chromium. Chrome, Edge, Opera e Brave o possuem. Safari e Firefox não. Sempre detecte a funcionalidade (feature-detect) com PerformanceObserver.supportedEntryTypes?.includes('long-animation-frame') antes de depender dele.

O LoAF substitui a Long Tasks API?

Para depuração de INP, sim. O Long Tasks fornece a duração do bloqueio na thread principal (main-thread) sem atribuição de script. O LoAF fornece o mesmo sinal de bloqueio mais o script real que rodou. Não há razão para usar o Long Tasks para novos trabalhos em INP. O Total Blocking Time pode eventualmente ser redefinido em termos do blockingDuration do LoAF.

Lendo dados do LoAF

Como eu correlaciono uma entrada de LoAF com uma medição específica de INP?

A build de atribuição do web-vitals.js faz isso por você via attribution.longAnimationFrameEntries. Ela encontra as entradas de LoAF cuja janela de tempo se sobrepõe à interação candidata ao INP. Se você estiver criando seu próprio observador, compare entry.startTime e entry.startTime + entry.duration com o tempo da interação, e inclua qualquer LoAF cuja janela se cruze.

O que forcedStyleAndLayoutDuration realmente significa?

É o tempo que o script passou disparando o layout síncrono. Quando seu JavaScript lê offsetHeight, getBoundingClientRect, ou qualquer outra propriedade que requer um layout atualizado após uma mutação no DOM, o navegador tem que fazer o trabalho de layout de forma síncrona dentro do script. Esse trabalho é atribuído de volta ao script. Um forcedStyleAndLayoutDuration alto é quase sempre layout thrashing.

Por que o array scripts está vazio em algumas entradas de 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á nenhuma atribuição. Segundo, scripts rodando dentro de iframes cross-origin, web workers, service workers ou extensões de navegador não são atribuídos porque o LoAF apenas vê a thread principal (main-thread) da mesma origem (same-origin). Terceiro, o frame pode não ter nenhum trabalho de script, apenas estilo e layout, o que é um frame de atraso de apresentação (presentation-delay).

Além do INP

O LoAF pode ajudar com o LCP, não apenas com o INP?

Sim, indiretamente. Um LoAF que roda antes que o elemento LCP seja renderizado é um script longo atrasando a renderização do LCP. Filtre os LoAFs para aqueles cujo tempo final é anterior ao timestamp do LCP, e o script mais longo ali é o maior contribuinte para o atraso de renderização do LCP. São os mesmos dados de atribuição, aplicados a uma métrica diferente.