Performance Core Web Vitals

Além do Lighthouse: o que o INP revela que o score 100 esconde

Lighthouse dá 100 e o usuário ainda reclama de lentidão. O INP mede o que o teste em laboratório não vê: a resposta do seu site a cada clique real.

· 11 min de leitura

Você roda o Lighthouse, tira 100 de performance, tira print e manda no grupo. Uma semana depois, o cliente escreve: "o site trava quando eu clico". Você abre, roda o Lighthouse de novo — 100. Está tudo verde. E ainda assim o usuário está certo: alguma coisa trava.

O Lighthouse não mentiu. Ele só não estava medindo o que o usuário sentiu.

Laboratório não é campo

Existem dois mundos na medição de performance, e confundi-los é a raiz do problema.

O laboratório (lab data) é o que o Lighthouse faz: carrega a página num ambiente controlado, uma vez, sem ninguém interagindo, e mede. É reproduzível e ótimo para pegar regressões — mas é uma simulação de carregamento, com um robô que nunca clica em nada.

O campo (field data, ou RUM — Real User Monitoring) é o que acontece com usuários de verdade: no celular de 2019, na rede 4G instável, clicando, digitando, abrindo menus. É a experiência real, e é isso que o Google usa para ranquear — o relatório CrUX (Chrome User Experience Report) coleta esses números dos usuários reais do Chrome.

O score 100 do Lighthouse é uma nota de laboratório sobre o carregamento. O INP é uma métrica de campo sobre a interação. Um site pode carregar voando e travar a cada clique — e é exatamente esse site que tira 100 e recebe reclamação.

O que o INP mede

INP é Interaction to Next Paint. Ele mede o tempo entre o usuário fazer algo — clicar, tocar, apertar uma tecla — e a tela mudar em resposta. Ou seja: quanto o seu site demora a dar sinal de vida quando alguém interage com ele.

Ele substituiu o antigo FID (First Input Delay) como Core Web Vital em 2024, e a troca foi uma dura correção de rota. O FID só media o atraso da primeira interação — um número fácil de manter baixo e que escondia quase todo o problema. O INP olha todas as interações da visita e reporta praticamente a pior delas, incluindo o tempo de processamento e o de renderização. É muito mais difícil de enganar.

Faixa de INPVeredito
Até 200 ms Bom — resposta imediata ao usuário
200 a 500 ms Precisa melhorar — a lentidão já é perceptível
Acima de 500 ms Ruim — parece travado

A meta é ficar abaixo de 200 ms no percentil 75 dos usuários — ou seja, rápido para 3 em cada 4 visitas, não só para a sua máquina de desenvolvimento.

Por que o Lighthouse não pega isso

Simples: o Lighthouse não interage com a página. Ele carrega, mede o carregamento e vai embora. INP mede resposta a cliques — e não houve nenhum clique. A métrica que o Lighthouse mostra no lugar é o TBT (Total Blocking Time), uma aproximação em laboratório de quão ocupada a thread principal ficou durante o carregamento.

TBT ajuda, mas mede uma janela estreita: só o carregamento inicial. O botão que trava é aquele que dispara um cálculo pesado na hora do clique, três minutos depois da página ter carregado — muito além do alcance do TBT. Por isso o site tira 100 e continua travando.

A causa quase sempre é a mesma: a thread principal

O JavaScript do navegador roda em uma única thread — a main thread. É a mesma thread que responde a cliques, roda os seus handlers e pinta a tela. Se você a ocupa com uma tarefa longa, nada mais acontece enquanto ela roda: o clique do usuário fica na fila, esperando.

botao.addEventListener('click', () => {
  // 400ms recalculando tudo, síncrono, na thread principal
  const resultado = recalcularPlanilhaInteira(dados);
  render(resultado);
  // o navegador só consegue repintar DEPOIS disto tudo
});

Enquanto recalcularPlanilhaInteira roda, a página está congelada: o clique já aconteceu, mas a tela não muda porque a thread está ocupada. Isso é INP alto — e o usuário descreve exatamente como "travou".

A regra mental do INP: toda tarefa longa na thread principal é um congelamento em potencial. O usuário não vê o seu código rodar — ele vê a tela parada. Interatividade é, antes de tudo, manter a thread principal livre para responder.

Três formas de devolver a thread ao usuário

1. Quebre a tarefa longa e ceda o controle

A correção mais direta é parar de fazer tudo de uma vez. Divida o trabalho em pedaços e ceda o controle ao navegador entre eles, para ele processar cliques pendentes e repintar:

// cede a thread ao navegador entre lotes
function cede() {
  return new Promise(r => setTimeout(r, 0));
  // (ou scheduler.yield() onde já houver suporte)
}

async function processar(itens) {
  for (let i = 0; i < itens.length; i++) {
    processaItem(itens[i]);
    if (i % 100 === 0) await cede();   // respira a cada 100
  }
}

Ceder a cada lote transforma uma tarefa de 400ms que congela tudo em várias tarefas curtas com espaços no meio — e é nesses espaços que o clique do usuário finalmente é atendido.

2. Renderize primeiro, calcule depois

Muitas vezes a resposta imediata que o usuário precisa é pequena — um estado de "carregando", o menu abrindo — e o trabalho pesado pode esperar o próximo quadro. Dê o retorno visual antes de começar o cálculo:

botao.addEventListener('click', () => {
  mostrarSpinner();              // resposta visual imediata → INP baixo

  requestAnimationFrame(() => {
    // o trabalho pesado roda DEPOIS de a tela já ter reagido
    const resultado = calcularPesado(dados);
    render(resultado);
  });
});

O INP mede até a próxima pintura. Se essa pintura for o spinner aparecendo, o INP é baixo — mesmo que o resultado final leve mais um segundo. O usuário sente que o site respondeu, porque ele respondeu.

3. Tire o trabalho pesado da thread com um Web Worker

Se o cálculo é realmente grande e puramente computacional, ele não deveria estar na thread da interface. Um Web Worker roda em outra thread, deixando a principal 100% livre para responder:

const worker = new Worker('calculo.js');

botao.addEventListener('click', () => {
  mostrarSpinner();
  worker.postMessage(dados);       // sai da main thread
});

worker.onmessage = (e) => render(e.data);   // volta pronto

A thread principal só despacha e recebe — o peso roda em paralelo, sem nunca bloquear um clique. É a solução mais robusta para trabalho pesado de verdade.

Você não pode consertar o que não mede

O ponto de partida não é otimizar no escuro — é medir campo, não só laboratório. Três caminhos, do mais simples ao mais completo:

import { onINP } from 'web-vitals';

onINP((metrica) => {
  // manda o INP real do usuario para o seu analytics
  enviarParaAnalytics('INP', metrica.value, metrica.rating);
});

Sem dado de campo, você está otimizando a sua própria máquina — que é rápida, tem boa rede e não representa o usuário no 4G. O laboratório te diz que carregou rápido; só o campo te diz se respondeu rápido.

Onde o Lighthouse ainda vale muito

Nada disto é contra o Lighthouse. Ele continua sendo a melhor ferramenta para o que faz:

O erro nunca foi usar o Lighthouse. Foi tratar o score 100 como prova de que o site é rápido para o usuário, quando ele só prova que carregou rápido no laboratório.

Em resumo

Lighthouse mede carregamento em laboratório; INP mede interação em campo. Um score 100 e um usuário reclamando de travamento não se contradizem — eles descrevem duas coisas diferentes, e só uma delas é a experiência real.

Interatividade se ganha mantendo a thread principal livre: quebre as tarefas longas e ceda o controle, dê o retorno visual antes do cálculo, e mande o peso de verdade para um Web Worker. Mas antes de qualquer linha disso, meça o campo — porque o número que importa é o que o usuário sente no clique, não o troféu verde na sua tela.