Um handler rápido pode conviver com uma interação lenta. O clique espera na fila antes de o callback começar, e a atualização visual ainda pode demorar depois que ele termina. Olhar apenas o tempo da função deixa dois trechos do caminho fora do diagnóstico.
Escolher entre reduzir trabalho, dividir tarefas, usar Workers ou rever o DOM exige acompanhar a interação inteira. A parcela dominante indica onde investigar e ajuda a evitar uma otimização que torna o código mais complexo sem reduzir a espera de quem usa a página.
O que termina quando o INP termina?
Interaction to Next Paint observa a latência de cliques, toques e interações de teclado ao longo da visita. O valor final representa uma das interações mais lentas, com uma regra para desconsiderar parte dos valores extremos em páginas muito interativas. Rolagem contínua e passar o mouse sobre um elemento não entram como interações dessa métrica.
O marco final é a próxima oportunidade de apresentação de um frame depois do processamento dos eventos. Uma busca pode mostrar “Buscando…” rapidamente e continuar esperando a resposta do servidor. Nesse caso, a interação pode ter INP baixo, embora a tarefa completa demore. Por isso, acompanhe também tempo até o resultado e falhas da operação. Um indicador que aparece rápido ajuda; um indicador eterno continua sendo um problema.
A referência de boa responsividade é INP de até 200 ms no percentil 75 das visitas, separando mobile e desktop. Isso orienta a avaliação do produto, não transforma 200 ms em orçamento disponível para cada função. O contexto de campo, discutido no artigo sobre Lighthouse e RUM, continua sendo o ponto de partida.
Três atrasos podem produzir o mesmo número
Uma interação tem três partes úteis para investigação: espera até os callbacks começarem, processamento dos eventos e espera até o próximo frame ser apresentado. O BEACON já mostrou por que olhar apenas o JavaScript do handler deixa parte do problema de fora. Separar essas parcelas ajuda a escolher uma correção para a causa encontrada.
Três interações de 480 ms podem ter distribuições como estas:
| Situação | Espera de entrada | Processamento | Apresentação | Primeira hipótese |
|---|---|---|---|---|
| A | 280 ms | 70 ms | 130 ms | Trabalho anterior ao clique |
| B | 30 ms | 350 ms | 100 ms | Trabalho iniciado pela interação |
| C | 30 ms | 70 ms | 380 ms | Atualização visual e renderização |
Na situação A, reduzir pela metade uma função de 70 ms preserva quase toda a espera. Na B, revisar o handler tem chance de atacar a parcela dominante. Na C, trocar uma conta matemática por um Worker pode deixar intacto o custo de atualizar uma lista enorme. O total é igual; a intervenção útil muda.
Encontre uma interação, uma rota e uma condição
“O site tem INP ruim” ainda é uma descrição larga demais. Procure algo como “abrir o filtro na listagem, em celulares intermediários, enquanto os dados iniciais ainda estão sendo processados”. Esse recorte combina uma ação reproduzível com um grupo afetado. Também ajuda a perceber regressões ligadas a um deploy ou a um script de terceiro.
A versão de atribuição da biblioteca web-vitals oferece as parcelas da interação candidata ao INP. Este exemplo registra somente tempos e uma etapa geral do carregamento, sem coletar texto do DOM, seletores ou URLs. Ele pressupõe a biblioteca no bundle e serve como ponto de partida para observação local:
import { onINP } from "web-vitals/attribution";
onINP(({ value, attribution }) => {
console.table({
inpMs: value,
esperaMs: attribution.inputDelay,
processamentoMs: attribution.processingDuration,
apresentacaoMs: attribution.presentationDelay,
carregamento: attribution.loadState,
});
});
Para levar isso a um coletor, associe identificadores de rota e ação definidos pela aplicação, além da versão do release. Prefira rótulos como “catalogo” e “abrir-filtros” a caminhos com IDs ou ao conteúdo digitado pelo usuário. Defina amostragem e retenção. O callback não é um registro completo de todos os cliques, e suas notificações não devem ser contadas automaticamente como visitas distintas.
No Chrome DevTools, grave o painel Performance durante a ação e selecione a interação na trilha correspondente. Compare as parcelas, procure as tarefas que atravessam esse intervalo e examine as pilhas de execução. Use source maps para chegar ao código da aplicação. CPU limitada ajuda a revelar sensibilidade a aparelhos lentos, mas o resultado continua sendo uma simulação: o sinal de campo aponta quem sofre; a gravação ajuda a explicar por quê.
Se o clique espera para começar, olhe para o que veio antes
Um handler curto pode ficar preso atrás da avaliação de scripts, da montagem inicial da interface ou de um callback que processa dados recebidos por fetch. O código responsável pelo bloqueio pode não ter relação aparente com o botão clicado. Otimizar só a função do botão deixa esse trabalho anterior na fila.
Se uma listagem recebe milhares de registros e os normaliza em uma única tarefa, um toque no filtro durante essa etapa precisa esperar. As alternativas começam por reduzir o conjunto recebido e eliminar processamento repetido. Depois, considere dividir a normalização em lotes ou mover o cálculo para outro contexto. Adiar todo o trabalho até o primeiro clique apenas troca o momento da cobrança.
Uma tarefa acima de 50 ms é classificada como longa. Esse limite ajuda a encontrar blocos de execução, mas não é uma garantia de fluidez: vários trechos menores, mais o trabalho visual, também podem atrasar a resposta. Confira a janela da interação inteira e a recorrência do bloqueio.
Se o processamento domina, separe urgência de volume
Ao abrir um filtro, o painel e a indicação do estado selecionado precisam aparecer logo. Recalcular uma exportação que ainda nem foi pedida provavelmente pode esperar. Antes de distribuir o código, descubra se ele precisa executar, quantas vezes executa e qual parte realmente é necessária para o próximo estado visível.
Quando uma sequência precisa continuar na main thread, scheduler.yield() permite ceder a execução e retomar em outra tarefa. O suporte deve ser detectado; uma Promise com setTimeout oferece um fallback com características de agendamento diferentes. Um processamento em lotes pode ficar assim:
async function cederExecucao() {
if (globalThis.scheduler?.yield) {
await globalThis.scheduler.yield();
return;
}
await new Promise((resolve) => setTimeout(resolve, 0));
}
async function normalizarEmLotes(registros, normalizar) {
const resultado = [];
let inicioDoLote = performance.now();
for (const registro of registros) {
resultado.push(normalizar(registro));
if (performance.now() - inicioDoLote >= 8) {
await cederExecucao();
inicioDoLote = performance.now();
}
}
return resultado;
}
O orçamento de 8 ms precisa ser ajustado ao aparelho, à quantidade de dados e ao custo de retomar a tarefa. Se uma única chamada de normalizar demorar 300 ms, a pausa só poderá ocorrer depois dela: será necessário dividir essa operação ou mudar onde ela executa. O array de entrada também deve permanecer estável durante o processamento.
Ceder execução não reduz automaticamente a quantidade total de trabalho e pode aumentar o tempo até terminar. Também abre espaço para novas ações do usuário entre lotes. Numa busca, uma solicitação recente pode tornar o cálculo anterior inútil; controle cancelamento ou uma versão da intenção antes de publicar o resultado. Declarar a função async não resolve por si só nem concorrência lógica nem trabalho síncrono pesado.
Usar await Promise.resolve() mantém a continuação na fila de microtarefas e não equivale a ceder para uma nova tarefa com oportunidade de renderização. requestAnimationFrame() também não é um depósito para cálculo pesado: seu callback participa do trabalho anterior à pintura. Colocar a mesma função cara nesses lugares pode preservar o travamento.
Quando um Worker compensa
Um Worker é candidato quando existe cálculo relevante que pode trabalhar sem acessar diretamente o DOM: transformar uma coleção grande, analisar texto ou preparar dados para visualização. Ele mantém esse cálculo fora da main thread, mas exige troca de mensagens, representação dos dados e coordenação dos resultados.
No filtro, enviar a base inteira a cada tecla pode gastar mais em transferência e cópia do que o esperado. Uma alternativa é manter os dados no Worker e enviar somente a consulta, identificando cada pedido para descartar respostas antigas. Quando aplicável, objetos transferíveis reduzem cópias, mas transferir um buffer também muda quem pode usá-lo.
O retorno do Worker ainda precisa virar interface. Se ele devolve dez mil itens e a aplicação insere dez mil linhas de uma vez, parte cara do trabalho continua no navegador principal. Avalie separadamente cálculo, comunicação e atualização visual. Um algoritmo pequeno e pouco frequente pode não justificar essa infraestrutura.
Se o frame demora a aparecer, revise a atualização visual
Na situação C da tabela, o callback acaba relativamente cedo. O intervalo seguinte pode envolver recalcular estilos, layout, pintura, outros callbacks e trabalho de composição. Atribuir toda essa parcela a CSS sem olhar a gravação seria outra forma de adivinhar.
Um padrão comum é alternar escrita de estilos e leitura de medidas dentro de um loop. Ao pedir uma medida que depende de mudanças recentes, o código pode forçar layout síncrono. Se vários cards precisam receber a largura de um painel, obtenha a medida necessária antes das escritas, desde que essa seja a medida desejada pela regra visual:
const largura = painel.offsetWidth;
for (const card of cards) {
card.style.width = `${largura}px`;
}
Nem toda leitura depois de uma escrita é dispensável; às vezes o código precisa da geometria nova. O objetivo é reunir leituras e escritas coerentes, evitando refazer a mesma conta de layout para cada item. Na gravação, procure os eventos de Layout e o código que os provocou.
Para listas extensas, paginação, virtualização e redução do alcance das atualizações atacam o volume visual. Virtualização exige cuidado com foco, navegação por teclado e acessibilidade. content-visibility pode reduzir renderização fora da área visível, mas não elimina os nós do DOM nem o cálculo de negócio. Escolha pela causa encontrada e confira os efeitos sobre o fluxo completo.
Uma correção precisa sobreviver à próxima visita
Antes de mudar o código, registre a hipótese: qual parcela domina, qual função ou atualização está envolvida e qual comportamento deve melhorar. Depois da alteração, repita a interação em condições comparáveis e confira também o tempo até o resultado útil. Mover trabalho para depois do próximo frame pode melhorar a resposta inicial e continuar entregando uma experiência ruim mais adiante.
No campo, compare o mesmo recorte de rota, dispositivo e release, observando volume de amostras e mudanças no público. Se a parcela dominante caiu e outra interação passou a ser a pior, o diagnóstico avançou. A próxima decisão já parte de uma causa identificada, em vez de mais uma rodada de otimizações por reflexo.
Fontes
- web.dev: definição e limites do INP
- web.dev: investigação e otimização das parcelas do INP
- GoogleChrome/web-vitals: build de atribuição
- Chrome DevTools: análise de performance em execução
- web.dev: tarefas longas e agendamento
- MDN: Scheduler.yield()
- web.dev: trabalho fora da main thread com Workers
- web.dev: layout e layout thrashing
- MDN: microtarefas e oportunidades de renderização

