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 INP | Veredito |
|---|---|
| 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:
- PageSpeed Insights. Além do laboratório, ele mostra os dados de campo do CrUX quando a página tem tráfego suficiente. É o jeito mais rápido de ver o seu INP real.
- Search Console → Core Web Vitals. Agrupa suas URLs em "boas", "a melhorar" e "ruins" com base em usuários reais. É onde o Google te mostra o que ele mesmo está usando para ranquear.
- A biblioteca
web-vitalsno seu código. Mede o INP de cada visita real e manda para onde você quiser. É a única forma de atribuir o INP alto a uma interação específica.
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:
- Pegar regressões antes de subir. Rodar Lighthouse no CI e barrar um deploy que piorou o carregamento é excelente uso — o ambiente controlado é justamente o que dá reprodutibilidade.
- Auditar carregamento (LCP) e boas práticas. Imagem sem dimensão, recurso sem cache, bundle gigante — o Lighthouse pega tudo isso com precisão.
- Acessibilidade e SEO técnico. As outras abas do relatório são um ótimo checklist automatizado.
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.