INP alto: como localizar o bloqueio e escolher a correção certa

Ilustração de um cursor ligado a blocos de processamento, engrenagens e camadas de uma página web.

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çãoEspera de entradaProcessamentoApresentaçãoPrimeira hipótese
A280 ms70 ms130 msTrabalho anterior ao clique
B30 ms350 ms100 msTrabalho iniciado pela interação
C30 ms70 ms380 msAtualizaçã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