1. Introdução
Uma demonstração interativa e um codelab para Aprenda Mais sobre a Interaction to Next Paint (INP).
Pré-requisitos
- Conhecimento de desenvolvimento em HTML e JavaScript.
- Recomendado: leia a documentação da INP.
Conteúdo do laboratório
- Como a combinação das interações do usuário e o processamento delas afetam a capacidade de resposta da página.
- Como reduzir e eliminar atrasos para uma experiência do usuário tranquila.
O que é necessário
- Um computador com a capacidade de clonar código do GitHub e executar comandos npm.
- Um editor de texto.
- Uma versão recente do Chrome para que todas as medições de interação funcionem.
2. Começar a configuração
Acessar e executar o código
O código está no repositório web-vitals-codelabs.
- Clone o repositório no terminal:
git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git - Acesse o diretório clonado:
cd web-vitals-codelabs/understanding-inp - Instale as dependências:
npm ci - Inicie o servidor da Web:
npm run start - Acesse http://localhost:5173/understanding-inp/ no navegador.
Visão geral do app
Na parte de cima da página, há um contador de pontuação e um botão Increment. Uma demonstração clássica de reatividade e responsividade.

Abaixo do botão, há quatro medições:
- INP: a pontuação atual de INP, que normalmente é a pior interação.
- Interação: a pontuação da interação mais recente.
- FPS: os quadros por segundo da linha de execução principal da página.
- Timer: uma animação de timer em execução para ajudar a visualizar o jank.
As entradas de QPS e Timer não são necessárias para medir interações. Eles são adicionados apenas para facilitar a visualização da responsividade.
Faça um teste
Tente interagir com o botão Increment e observe o aumento da pontuação. Os valores de INP e Interação mudam a cada incremento?
A INP mede o tempo que leva desde o momento em que o usuário interage até que a página mostre a atualização renderizada para o usuário.
3. Como medir interações com o Chrome DevTools
Abra o DevTools no menu Mais ferramentas > Ferramentas para desenvolvedores, clicando com o botão direito do mouse na página e selecionando Inspecionar ou usando um atalho do teclado.
Mude para o painel Performance, que será usado para medir as interações.

Em seguida, capture uma interação no painel "Performance".
- pressione "Gravar".
- Interaja com a página (pressione o botão Increment).
- Interrompa a gravação.
Na linha do tempo resultante, você vai encontrar uma faixa de Interações. Clique no triângulo à esquerda para expandir.

Duas interações aparecem. Aumente o zoom na segunda, rolando a tela ou mantendo a tecla W pressionada.

Ao passar o cursor sobre a interação, você pode ver que ela foi rápida, sem gastar tempo na duração do processamento e com um tempo mínimo de atraso de entrada e atraso de apresentação, cujas durações exatas dependem da velocidade da sua máquina.
4. Listeners de eventos de longa duração
Abra o arquivo index.js e remova a marca de comentário da função blockFor dentro do listener de eventos.
Confira o código completo: click_block.html
button.addEventListener('click', () => {
blockFor(1000);
score.incrementAndUpdateUI();
});
Salve o arquivo. O servidor vai detectar a mudança e atualizar a página para você.
Tente interagir com a página de novo. As interações agora serão visivelmente mais lentas.
Trace de performance
Faça outra gravação no painel "Performance" para ver como isso aparece lá.

O que antes era uma interação curta agora leva um segundo inteiro.
Ao passar o cursor sobre a interação, observe que o tempo é quase todo gasto em "Duração do processamento", que é o tempo necessário para executar os callbacks do listener de eventos. Como a chamada de bloqueio blockFor está totalmente dentro do listener de eventos, é aí que o tempo é gasto.
5. Experimento: duração do processamento
Teste maneiras de reorganizar o trabalho do listener de eventos para ver o efeito na INP.
Atualizar a interface primeiro
O que acontece se você trocar a ordem das chamadas JS, atualizando primeiro a interface e depois bloqueando?
Confira o código completo: ui_first.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
blockFor(1000);
});
Você percebeu que a interface apareceu antes? A ordem afeta as pontuações de INP?
Tente fazer um trace e examinar a interação para ver se há diferenças.
Listeners separados
E se você mover o trabalho para um listener de eventos separado? Atualize a interface em um listener de eventos e bloqueie a página em um listener separado.
Veja o código completo: two_click.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('click', () => {
blockFor(1000);
});
Como ele aparece no painel de desempenho agora?
Tipos de evento diferentes
A maioria das interações aciona muitos tipos de eventos, desde eventos de ponteiro ou de tecla até eventos de passagem, foco/desfoque e eventos sintéticos, como beforechange e beforeinput.
Muitas páginas reais têm listeners para vários eventos diferentes.
O que acontece se você mudar os tipos de evento para os listeners de eventos? Por exemplo, substituir um dos listeners de eventos click por pointerup ou mouseup?
Confira o código completo: diff_handlers.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('pointerup', () => {
blockFor(1000);
});
Nenhuma atualização da interface
O que acontece se você remover a chamada para atualizar a IU do listener de eventos?
Veja o código completo: no_ui.html
button.addEventListener('click', () => {
blockFor(1000);
// score.incrementAndUpdateUI();
});
6. Resultados do experimento de duração do processamento
Trace de desempenho: atualizar a interface primeiro
Confira o código completo: ui_first.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
blockFor(1000);
});
Ao analisar uma gravação do painel "Desempenho" do clique no botão, é possível notar que os resultados não mudaram. Embora uma atualização da interface tenha sido acionada antes do código de bloqueio, o navegador não atualizou o que foi pintado na tela até que o listener de eventos fosse concluído, o que significa que a interação ainda levou pouco mais de um segundo para ser concluída.

Trace de desempenho: listeners separados
Veja o código completo: two_click.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('click', () => {
blockFor(1000);
});
Não há diferença funcional. A interação ainda leva um segundo inteiro.
Se você ampliar a interação de clique, vai notar que há duas funções diferentes sendo chamadas como resultado do evento click.
Como esperado, a primeira (atualização da interface) é executada de forma incrivelmente rápida, enquanto a segunda leva um segundo inteiro. No entanto, a soma dos efeitos resulta na mesma interação lenta para o usuário final.

Trace de desempenho: diferentes tipos de evento
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('pointerup', () => {
blockFor(1000);
});
Esses resultados são muito semelhantes. A interação ainda leva um segundo inteiro. A única diferença é que o listener click, que é mais curto e atualiza apenas a IU, agora é executado depois do listener pointerup, que é bloqueante.

Trace de desempenho: sem atualização da interface
Veja o código completo: no_ui.html
button.addEventListener('click', () => {
blockFor(1000);
// score.incrementAndUpdateUI();
});
- A pontuação não é atualizada, mas a página sim.
- Animações, efeitos CSS, ações padrão de componentes da Web (entrada de formulário), entrada de texto e destaque de texto continuam sendo atualizados.
Nesse caso, o botão passa para um estado ativo e volta quando clicado, o que exige uma pintura pelo navegador, ou seja, ainda há uma INP.
Como o listener de eventos bloqueou a linha de execução principal por um segundo, impedindo que a página fosse renderizada, a interação ainda leva um segundo inteiro.
Uma gravação do painel "Performance" mostra que a interação é praticamente idêntica às anteriores.

Takeaway
Qualquer código em execução em qualquer listener de eventos vai atrasar a interação.
- Isso inclui listeners registrados de diferentes scripts e códigos de framework ou biblioteca que são executados em listeners, como uma atualização de estado que aciona uma renderização de componente.
- Não apenas o seu código, mas também todos os scripts de terceiros.
É um problema comum.
Por fim, só porque seu código não aciona uma pintura, não significa que uma pintura não vai esperar que os listeners de eventos lentos sejam concluídos.
7. Experimento: latência na entrada
E quanto ao código de longa duração fora dos listeners de eventos? Exemplo:
- Se você tiver um
<script>de carregamento tardio que bloqueia a página aleatoriamente durante o carregamento. - Uma chamada de API, como
setInterval, que bloqueia periodicamente a página?
Remova o blockFor do listener de eventos e adicione-o a um setInterval():
Confira o código completo: input_delay.html
setInterval(() => {
blockFor(1000);
}, 3000);
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
O que acontece?
8. Resultados do experimento de latência na entrada
Confira o código completo: input_delay.html
setInterval(() => {
blockFor(1000);
}, 3000);
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
Registrar um clique em um botão que ocorre enquanto a tarefa de bloqueio setInterval está em execução resulta em uma interação de longa duração, mesmo que nenhum trabalho de bloqueio esteja sendo feito na interação em si.
Esses períodos de longa duração costumam ser chamados de tarefas longas.
Ao passar o cursor sobre a interação no DevTools, você vai notar que o tempo de interação agora é atribuído principalmente ao atraso de entrada, não à duração do processamento.

Observe que isso nem sempre afeta as interações. Se você não clicar enquanto a tarefa estiver em execução, talvez tenha sorte. Esses espirros "aleatórios" podem ser um pesadelo para depurar quando causam problemas apenas algumas vezes.
Uma maneira de rastrear esses problemas é medir tarefas longas (ou frames longos de animação) e o Total Blocking Time.
9. Apresentação lenta
Até agora, analisamos o desempenho do JavaScript, por meio de atraso de entrada ou listeners de eventos, mas o que mais afeta a INP?
Atualizando a página com efeitos caros.
Mesmo que a atualização da página seja rápida, o navegador ainda pode ter que trabalhar muito para renderizá-la.
Na linha de execução principal:
- Frameworks de interface que precisam renderizar atualizações após mudanças de estado
- Mudanças no DOM ou a alternância de muitos seletores de consulta CSS caros podem acionar muitos estilos, layouts e pinturas.
Fora da linha de execução principal:
- Como usar CSS para potencializar os efeitos da GPU
- Adicionar imagens muito grandes em alta resolução
- Como usar SVG/Canvas para desenhar cenas complexas

Alguns exemplos comuns encontrados na Web:
- Um site de SPA que recria todo o DOM depois de clicar em um link, sem pausar para fornecer um feedback visual inicial.
- Uma página de pesquisa que oferece filtros de pesquisa complexos com uma interface do usuário dinâmica, mas que executa listeners caros para fazer isso.
- Um botão de modo escuro que aciona o estilo/layout de toda a página
10. Experimento: atraso na apresentação
requestAnimationFrame lento
Vamos simular um longo atraso de apresentação usando a API requestAnimationFrame().
Mova a chamada blockFor para um callback requestAnimationFrame para que ela seja executada depois que o listener de eventos retornar:
Veja o código completo: presentation_delay.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
blockFor(1000);
});
});
O que acontece?
11. Resultados do experimento de atraso na apresentação
Veja o código completo: presentation_delay.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
blockFor(1000);
});
});
A interação continua com um segundo de duração, então o que aconteceu?
requestAnimationFrame solicita um callback antes da próxima pintura. Como a INP mede o tempo entre a interação e a próxima pintura, o blockFor(1000) no requestAnimationFrame continua bloqueando a próxima pintura por um segundo inteiro.

No entanto, observe duas coisas:
- Ao passar o cursor, você vai notar que todo o tempo de interação agora está sendo gasto em "atraso de apresentação", já que o bloqueio da linha de execução principal está acontecendo depois que o listener de eventos retorna.
- A raiz da atividade da linha de execução principal não é mais o evento de clique, mas sim "Animation Frame Fired".
12. Como diagnosticar interações
Nesta página de teste, a responsividade é super visual, com as pontuações, os timers e a interface do contador, mas ao testar a página média, ela é mais sutil.
Quando as interações demoram, nem sempre fica claro qual é o culpado. É:
- Latência na entrada?
- Duração do processamento de eventos?
- Atraso na apresentação?
Em qualquer página, você pode usar o DevTools para medir a responsividade. Para criar o hábito, siga este fluxo:
- Navegue na Web normalmente.
- Fique de olho no registro de interações na visualização de métricas em tempo real do painel "Performance" do DevTools.
- Se você encontrar uma interação com desempenho ruim, tente repeti-la:
- Se não for possível repetir, use o registro de interações para receber insights.
- Se for possível repetir, registre um rastro no painel "Performance".
Todos os atrasos
Tente adicionar um pouco de todos esses problemas à página:
Confira o código completo: all_the_things.html
setInterval(() => {
blockFor(1000);
}, 3000);
button.addEventListener('click', () => {
blockFor(1000);
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
blockFor(1000);
});
});
Em seguida, use o console e o painel de desempenho para diagnosticar os problemas.
13. Experimento: trabalho assíncrono
Como é possível iniciar efeitos não visuais dentro de interações, como fazer solicitações de rede, iniciar timers ou apenas atualizar o estado global, o que acontece quando eles eventualmente atualizam a página?
Desde que a próxima pintura após uma interação possa ser renderizada, mesmo que o navegador decida que não precisa de uma nova atualização de renderização, a medição da interação é interrompida.
Para testar isso, continue atualizando a IU no listener de cliques, mas execute o trabalho de bloqueio no tempo limite.
Confira o código completo: timeout_100.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
setTimeout(() => {
blockFor(1000);
}, 100);
});
O que acontece agora?
14. Resultados do experimento de trabalho assíncrono
Confira o código completo: timeout_100.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
setTimeout(() => {
blockFor(1000);
}, 100);
});

A interação agora é curta porque a linha de execução principal fica disponível imediatamente após a atualização da interface. A tarefa de bloqueio longa ainda é executada, mas só depois da exibição, para que o usuário receba feedback imediato da interface.
Lição: se não for possível remover, pelo menos mova.
Métodos
Podemos fazer melhor do que um setTimeout fixo de 100 milissegundos? Provavelmente, ainda queremos que o código seja executado o mais rápido possível. Caso contrário, bastaria removê-lo.
Meta:
- A interação vai executar
incrementAndUpdateUI(). blockFor()será executado assim que possível, mas não vai bloquear a próxima pintura.- Isso resulta em um comportamento previsível sem "tempos limite mágicos".
Algumas maneiras de fazer isso envolvem:
setTimeout(0)Promise.then()requestAnimationFramerequestIdleCallbackscheduler.postTask()
"requestPostAnimationFrame"
Ao contrário de requestAnimationFrame por conta própria (que tenta ser executado antes da próxima pintura e geralmente ainda resulta em uma interação lenta), requestAnimationFrame + setTimeout cria um polyfill simples para requestPostAnimationFrame, executando o callback depois da próxima pintura.
Veja o código completo: raf+task.html
function afterNextPaint(callback) {
requestAnimationFrame(() => {
setTimeout(callback, 0);
});
}
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
afterNextPaint(() => {
blockFor(1000);
});
});
Para facilitar, você pode até mesmo envolvê-lo em uma promessa:
Veja o código completo: raf+task2.html
async function nextPaint() {
return new Promise(resolve => afterNextPaint(resolve));
}
button.addEventListener('click', async () => {
score.incrementAndUpdateUI();
await nextPaint();
blockFor(1000);
});
15. Várias interações (e cliques de raiva)
Mover o trabalho de bloqueio longo pode ajudar, mas essas tarefas longas ainda bloqueiam a página, afetando as interações futuras, bem como muitas outras animações e atualizações da página.
Teste a versão da página com trabalho de bloqueio assíncrono novamente (ou a sua própria, se você tiver criado uma variação para adiar o trabalho na última etapa):
Confira o código completo: timeout_100.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
setTimeout(() => {
blockFor(1000);
}, 100);
});
O que acontece se você clicar várias vezes rapidamente?
Trace de performance
Para cada clique, há uma tarefa de um segundo na fila, garantindo que a linha de execução principal seja bloqueada por um período considerável.

Quando essas tarefas longas se sobrepõem a novos cliques, as interações ficam lentas, mesmo que o listener de eventos em si retorne quase imediatamente. Criamos a mesma situação do experimento anterior com atrasos de entrada. Só que, desta vez, o atraso de entrada não vem de um setInterval, mas de um trabalho acionado por listeners de eventos anteriores.
Estratégias
O ideal é remover as tarefas longas por completo.
- Remova totalmente o código desnecessário, especialmente os scripts.
- Otimize o código para evitar a execução de tarefas longas.
- Interrompa o trabalho desatualizado quando novas interações chegarem.
16. Estratégia 1: debounce
Uma estratégia clássica. Sempre que as interações chegarem em rápida sucessão e os efeitos de processamento ou de rede forem caros, atrase o início do trabalho de propósito para que você possa cancelar e reiniciar. Esse padrão é útil para interfaces do usuário, como campos de preenchimento automático.
- Use
setTimeoutpara atrasar o início de um trabalho caro com um timer, talvez de 500 a 1.000 milissegundos. - Salve o ID do timer ao fazer isso.
- Se uma nova interação chegar, cancele o timer anterior usando
clearTimeout.
Confira o código completo: debounce.html
let timer;
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
blockFor(1000);
}, 1000);
});
Trace de performance

Apesar de vários cliques, apenas uma tarefa blockFor é executada, aguardando até que não haja cliques por um segundo inteiro antes de ser executada. Para interações que ocorrem em sequência, como digitar em uma entrada de texto ou itens que devem receber vários cliques rápidos, essa é uma estratégia ideal para usar por padrão.
17. Estratégia 2: interromper o trabalho de longa duração
Ainda há uma chance de que um clique ocorra logo após o período de debounce, caia no meio dessa tarefa longa e se torne uma interação muito lenta devido ao atraso de entrada.
O ideal é que, se uma interação ocorrer no meio da nossa tarefa, possamos pausar o trabalho para que as novas interações sejam processadas imediatamente. Como podemos fazer isso?
Existem algumas APIs como isInputPending, mas geralmente é melhor dividir tarefas longas em partes.
Muitos setTimeouts
Primeira tentativa: faça algo simples.
Veja o código completo: small_tasks.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
setTimeout(() => blockFor(100), 0);
});
});
Isso funciona permitindo que o navegador programe cada tarefa individualmente, e a entrada pode ter prioridade mais alta.

Voltamos a ter cinco segundos de trabalho para cinco cliques, mas cada tarefa de um segundo por clique foi dividida em dez tarefas de 100 milissegundos. Como resultado, mesmo com várias interações sobrepostas a essas tarefas, nenhuma interação tem um atraso de entrada de mais de 100 milissegundos. O navegador prioriza os listeners de eventos recebidos em relação ao trabalho de setTimeout, e as interações continuam responsivas.
Essa estratégia funciona muito bem ao programar pontos de entrada separados, como quando você tem vários recursos independentes que precisam ser chamados no momento do carregamento do aplicativo. Carregar scripts e executar tudo no momento da avaliação do script pode executar tudo em uma tarefa longa gigante por padrão.
No entanto, essa estratégia não funciona tão bem para separar códigos com acoplamento rígido, como um loop for que usa estado compartilhado.
Agora com yield()
No entanto, podemos usar os modernos async e await para adicionar facilmente "pontos de rendimento" a qualquer função JavaScript.
Exemplo:
Confira o código completo: yieldy.html
// Polyfill for scheduler.yield()
async function schedulerDotYield() {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
async function blockInPiecesYieldy(ms) {
const ms_per_part = 10;
const parts = ms / ms_per_part;
for (let i = 0; i < parts; i++) {
await schedulerDotYield();
blockFor(ms_per_part);
}
}
button.addEventListener('click', async () => {
score.incrementAndUpdateUI();
await blockInPiecesYieldy(1000);
});
Como antes, a linha de execução principal é liberada após um bloco de trabalho, e o navegador pode responder a qualquer interação recebida. No entanto, agora tudo o que é necessário é um await schedulerDotYield() em vez de setTimeouts separados, o que o torna ergonômico o suficiente para ser usado até mesmo no meio de um loop for.
Agora com AbortContoller()
Isso funcionou, mas cada interação programa mais trabalho, mesmo que novas interações tenham chegado e possam ter mudado o trabalho que precisa ser feito.
Com a estratégia de debouncing, cancelamos o tempo limite anterior a cada nova interação. Podemos fazer algo semelhante aqui? Uma maneira de fazer isso é usar um AbortController():
Veja o código completo: aborty.html
// Polyfill for scheduler.yield()
async function schedulerDotYield() {
return new Promise(resolve => {
setTimeout(resolve, 0);
});
}
async function blockInPiecesYieldyAborty(ms, signal) {
const parts = ms / 10;
for (let i = 0; i < parts; i++) {
// If AbortController has been asked to stop, abandon the current loop.
if (signal.aborted) return;
await schedulerDotYield();
blockFor(10);
}
}
let abortController = new AbortController();
button.addEventListener('click', async () => {
score.incrementAndUpdateUI();
abortController.abort();
abortController = new AbortController();
await blockInPiecesYieldyAborty(1000, abortController.signal);
});
Quando um clique é recebido, ele inicia o loop blockInPiecesYieldyAborty for, que faz o trabalho necessário e, ao mesmo tempo, libera periodicamente a linha de execução principal para que o navegador continue responsivo a novas interações.
Quando um segundo clique é recebido, o primeiro loop é sinalizado como cancelado com o AbortController, e um novo loop blockInPiecesYieldyAborty é iniciado. Na próxima vez que o primeiro loop for programado para ser executado novamente, ele vai notar que signal.aborted agora é true e vai retornar imediatamente sem fazer mais nada.

18. Conclusão
Dividir todas as tarefas longas permite que um site responda a novas interações. Isso permite que você forneça feedback inicial rapidamente e tome decisões, como cancelar o trabalho em andamento. Às vezes, isso significa programar pontos de entrada como tarefas separadas. Às vezes, isso significa adicionar pontos de "rendimento" onde for conveniente.
Vale lembrar
- A INP mede todas as interações.
- Cada interação é medida da entrada até a próxima exibição, que é como o usuário percebe a capacidade de resposta.
- A latência na entrada, a duração do processamento de eventos e o atraso na apresentação afetam a capacidade de resposta da interação.
- É fácil medir a INP e as interações com o DevTools.
Estratégias
- Não use códigos de longa duração (tarefas longas) nas suas páginas.
- Mova o código desnecessário para fora dos listeners de eventos até depois da próxima pintura.
- Verifique se a atualização de renderização é eficiente para o navegador.