1. Introdução
Uma demonstração interativa e um codelab para aprender sobre Interação com a Next Paint (INP).
Pré-requisitos
- Conhecimento de desenvolvimento em HTML e JavaScript.
- Recomendado: leia a documentação do INP.
Conteúdo do laboratório
- Como a interação das interações do usuário e seu tratamento dessas interações afetam a capacidade de resposta da página.
- Como reduzir e eliminar atrasos para proporcionar uma experiência tranquila ao usuário.
O que é necessário
- Um computador com a capacidade de clonar o 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
Receber e executar o código
O código pode ser encontrado no repositório web-vitals-codelabs
.
- Clone o repositório no seu terminal:
git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git
- Acesse o diretório clonado:
cd web-vitals-codelabs/understanding-inp
- Instalar 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
No topo da página, estão o contador Pontuação e o botão Incrementar. Uma demonstração clássica de reatividade e capacidade de resposta!
Abaixo do botão, há quatro medidas:
- INP: a pontuação INP atual, que normalmente é a pior interação.
- Interação: a pontuação da interação mais recente.
- QPS: os principais frames por segundo da linha de execução da página.
- Cronômetro: uma animação de cronômetro em execução para ajudar a visualizar a instabilidade.
As entradas de QPS e cronômetro não são necessárias para medir interações. Elas foram adicionadas apenas para facilitar a visualização da capacidade de resposta.
Faça um teste
Tente interagir com o botão Incrementar e veja a pontuação aumentar. Os valores INP e Interaction mudam a cada incremento?
O INP mede quanto tempo leva do momento em que o usuário interage até que a página realmente mostre a atualização renderizada ao usuário.
3. Medir as interações com o Chrome DevTools
Abra o DevTools em Mais ferramentas > No menu Ferramentas para desenvolvedores, clique com o botão direito do mouse na página e selecione Inspecionar ou use um atalho do teclado.
Alterne para o painel Desempenho, que você usará para medir as interações.
Em seguida, capture uma interação no painel "Desempenho".
- pressione "Gravar".
- Interaja com a página (pressione o botão Aumentar).
- Pare a gravação.
Na linha do tempo resultante, você vai encontrar a faixa Interactions. Clique no triângulo do lado esquerdo para expandi-lo.
Duas interações são exibidas. Aumente o zoom no segundo rolando ou segurando a tecla W.
Passe o cursor sobre a interação e observe que ela foi rápida, sem gastar tempo na duração do processamento e de uma quantidade mínima de tempo em atraso de entrada e atraso da apresentação. A duração exata depende da velocidade da 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.
Veja o código completo: click_block.html
button.addEventListener('click', () => {
blockFor(1000);
score.incrementAndUpdateUI();
});
Salve o arquivo. O servidor verá a alteração e atualizará a página para você.
Tente interagir com a página de novo. As interações serão visivelmente mais lentas.
Rastreamento de desempenho
Faça outra gravação no painel "Desempenho" para ver o resultado.
O que antes era uma interação curta agora leva um segundo.
Ao passar o cursor sobre a interação, observe que o tempo é quase todo gasto em "Duração do processamento", que é a quantidade de tempo necessária para executar os callbacks do listener de eventos. Como a chamada de bloqueio blockFor
está inteiramente dentro do listener de eventos, esse é para onde o tempo vai.
5. Experimento: duração do processamento
Tente maneiras de reorganizar o trabalho do listener de eventos para ver o efeito no INP.
Atualizar a interface primeiro
O que acontece se você trocar a ordem das chamadas js: atualizar a interface primeiro e depois bloquear?
Veja o código completo: ui_first.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
blockFor(1000);
});
Você notou que a interface aparece mais cedo? A ordem afeta as pontuações de INP?
Tente fazer um rastreamento e examinar a interação para ver se há alguma diferença.
Listeners separados
E se você mover o trabalho para um listener de eventos separado? Atualizar a IU em um listener de eventos e bloquear a página de um listener separado.
Consulte o código completo: two_click.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('click', () => {
blockFor(1000);
});
Como ele aparece no painel de desempenho?
Diferentes tipos de evento
A maioria das interações vai disparar vários tipos de eventos, desde eventos de ponteiro ou teclas até passar o cursor, focar/desfocar e eventos sintéticos, como beforechange e beforeinput.
Muitas páginas reais têm listeners para muitos eventos diferentes.
O que acontece se você alterar os tipos de evento para os listeners de eventos? Por exemplo, substituir um dos listeners de eventos click
por pointerup
ou mouseup
?
Consulte o código completo: diff_handlers.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('pointerup', () => {
blockFor(1000);
});
Nenhuma atualização de interface
O que acontece se você remover a chamada para atualizar a interface 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
Rastreamento de desempenho: atualize a interface primeiro
Veja o código completo: ui_first.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
blockFor(1000);
});
Observando uma gravação do Painel de desempenho de clicar no botão, você pode ver que os resultados não mudaram. Embora uma atualização de IU 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.
Rastreamento de desempenho: listeners separados
Consulte o código completo: two_click.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('click', () => {
blockFor(1000);
});
Novamente, não há diferença funcional. A interação ainda leva um segundo inteiro.
Se você aumentar o zoom na interação de clique, vai notar que há de fato duas funções diferentes sendo chamadas como resultado do evento click
.
Como esperado, a primeira (atualização da interface) é executada incrivelmente rápido, enquanto a segunda leva um segundo. No entanto, a soma dos efeitos resulta na mesma interação lenta para o usuário final.
Rastreamento de desempenho: diferentes tipos de eventos
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
button.addEventListener('pointerup', () => {
blockFor(1000);
});
Esses resultados são muito semelhantes. A interação ainda dura um segundo inteiro; A única diferença é que o listener click
mais curto somente atualização da interface agora é executado após o listener pointerup
de bloqueio.
Rastreamento de desempenho: sem atualização de 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 é atualizada.
- Animações, efeitos de CSS, ações padrão do componente da Web (entrada de formulário), entrada de texto e destaque de texto continuam sendo atualizados.
Nesse caso, o botão vai para um estado ativo e volta quando clicado, o que exige uma pintura do navegador, o que significa que ainda há um INP.
Como o listener de eventos bloqueou a linha de execução principal por um segundo, impedindo que a página seja pintada, a interação ainda leva um segundo inteiro.
Fazer uma gravação do Painel de desempenho mostra a interação praticamente idêntica à anterior.
Para viagem
Qualquer código em execução no listener de eventos any vai atrasar a interação.
- Isso inclui listeners registrados de diferentes scripts e códigos de framework ou biblioteca executados em listeners, como uma atualização de estado que aciona a renderização de um componente.
- Não apenas seu próprio código, mas também todos os scripts de terceiros.
Isso é um problema comum.
Por fim, o fato de o código não acionar uma pintura não significa que ela não estará esperando que os listeners de eventos lentos sejam concluídos.
7. Experimento: atraso de entrada
E quanto ao código de longa duração fora dos listeners de eventos? Exemplo:
- Se você tinha um
<script>
de carregamento atrasado que bloqueou aleatoriamente a página durante o carregamento. - Uma chamada de API, como
setInterval
, que bloqueia periodicamente a página?
Tente remover o blockFor
do listener de eventos e adicioná-lo a um setInterval()
:
Consulte o código completo: input_delay.html
setInterval(() => {
blockFor(1000);
}, 3000);
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
O que acontece?
8. Resultados do experimento de atraso de entrada
Consulte o código completo: input_delay.html
setInterval(() => {
blockFor(1000);
}, 3000);
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
});
A gravação de um clique de 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 seja feito na própria interação.
Esses períodos de longa duração costumam ser chamados de tarefas longas.
Passe o cursor sobre a interação no DevTools para ver 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 quando a tarefa estiver em execução, talvez tenha sorte. Tão "aleatório" espirros pode ser um pesadelo de depuração quando só às vezes causam problemas.
Uma maneira de rastreá-los é medindo tarefas longas (ou frames de animação longos) e tempo total de bloqueio.
9. Apresentação lenta
Até agora, analisamos o desempenho do JavaScript por meio de atrasos de entrada ou listeners de eventos. No entanto, o que mais afeta a renderização da próxima pintura?
Bem, atualizando a página com efeitos caros!
Mesmo que a atualização da página seja feita rapidamente, o navegador pode ter que se esforçar 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 vários estilos, layouts e pinturas.
Fora da linha de execução principal:
- Como usar CSS para potencializar efeitos de GPU
- Adicionar imagens de alta resolução muito grandes
- Uso de SVG/Canvas para desenhar cenas complexas
Alguns exemplos comumente encontrados na Web:
- Um site de SPA que recria todo o DOM após 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 de usuário dinâmica, mas executa listeners caros para isso.
- Um botão para ativar/desativar o modo escuro que aciona estilo/layout para toda a página
10. Experimento: atraso na apresentação
requestAnimationFrame
lento
Vamos simular um longo atraso na 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:
Consulte o código completo: presentation_delay.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
blockFor(1000);
});
});
O que acontece?
11. Resultados do experimento de atraso da apresentação
Consulte o código completo: presentation_delay.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
blockFor(1000);
});
});
A interação permanece 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 da interação até 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ê verá que todo o tempo de interação está sendo gasto em "atraso da 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 "Frame de animação disparado".
12. Como diagnosticar interações
Nesta página de teste, a capacidade de resposta é supervisual, com pontuações, timers e a interface do contador...mas ao testar a página média ela é mais sutil.
Quando as interações são longas, nem sempre fica claro qual é o culpado. Ele é:
- Atraso na entrada?
- Duração do processamento do evento?
- Atraso na apresentação?
Em qualquer página que quiser, você pode usar o DevTools para ajudar a medir a capacidade de resposta. Para criar o hábito, siga este fluxo:
- Navegue na Web normalmente.
- Opcional: deixe o console do DevTools aberto enquanto a extensão das Core Web Vitals registra as interações.
- Se você encontrar uma interação com baixo desempenho, tente repeti-la:
- Se não for possível repetir a ação, use os registros do console para receber insights.
- Se puder repetir, grave no painel de desempenho.
Todos os atrasos
Tente adicionar alguns destes problemas à página:
Veja o código completo: all_the_Things.html
setInterval(() => {
blockFor(1000);
}, 3000);
button.addEventListener('click', () => {
blockFor(1000);
score.incrementAndUpdateUI();
requestAnimationFrame(() => {
blockFor(1000);
});
});
Depois, use o console e o painel de desempenho para diagnosticar os problemas.
13. Experimento: trabalho assíncrono
Já que é 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 atualizam a página depois?
Enquanto a next paint após uma interação puder ser renderizada, mesmo que o navegador decida que não precisa de uma nova atualização de renderização, a medição de interações será interrompida.
Para testar isso, continue atualizando a interface pelo listener de clique, mas execute o trabalho de bloqueio até o tempo limite.
Consulte o código completo: tempo limite_100.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
setTimeout(() => {
blockFor(1000);
}, 100);
});
O que acontecerá agora?
14. Resultados de experimentos de trabalho assíncronos
Consulte o código completo: tempo limite_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. Ela apenas é executada algum tempo após a pintura, de modo que o usuário recebe feedback imediato da interface.
Lição: se não puder removê-lo, pelo menos mova-o!
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, ele deve ter sido removido.
Meta:
- A interação executará
incrementAndUpdateUI()
. blockFor()
será executado o mais rápido possível, mas não 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()
requestAnimationFrame
requestIdleCallback
scheduler.postTask()
"requestPostAnimationFrame"
Ao contrário de requestAnimationFrame
sozinho, que tentará ser executado antes da próxima pintura e geralmente ainda terá uma interação lenta, requestAnimationFrame
+ setTimeout
criam um polyfill simples para requestPostAnimationFrame
, executando o callback após a próxima pintura.
Consulte o código completo: raf+task.html
function afterNextPaint(callback) {
requestAnimationFrame(() => {
setTimeout(callback, 0);
});
}
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
afterNextPaint(() => {
blockFor(1000);
});
});
Para ergonomia, você pode até mesmo incluir uma promessa:
Consulte 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 uma solução longa de bloqueio pode ajudar, mas essas tarefas longas ainda bloqueiam a página, afetando interações futuras, assim como muitas outras animações e atualizações da página.
Teste a versão de trabalho de bloqueio assíncrono da página novamente (ou a sua, se você tiver uma variação sobre o adiamento do trabalho na última etapa):
Consulte o código completo: tempo limite_100.html
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
setTimeout(() => {
blockFor(1000);
}, 100);
});
O que acontece se você clicar várias vezes rapidamente?
Rastreamento de desempenho
Para cada clique, há uma tarefa de um segundo na fila, garantindo que a linha de execução principal fique bloqueada por um período significativo.
Quando essas tarefas longas se sobrepõem à chegada de novos cliques, isso resulta em interações lentas, mesmo que o próprio listener de eventos retorne quase imediatamente. Criamos a mesma situação do experimento anterior com atrasos de entrada. Apenas desta vez, o atraso de entrada não está vindo de um setInterval
, mas sim do trabalho acionado por listeners de eventos anteriores.
Estratégias
O ideal é remover completamente as tarefas longas.
- Remova completamente o código desnecessário, especialmente scripts.
- Otimize o código para evitar a execução de tarefas longas.
- Cancela o trabalho desatualizado quando novas interações chegam.
16. Estratégia 1: debounce
Uma estratégia clássica. Sempre que as interações ocorrerem 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
setTimeout
para atrasar o início de trabalhos caros, com um timer de 500 a 1.000 milissegundos. - Quando fizer isso, salve o ID do timer.
- Se uma nova interação chegar, cancele o timer anterior usando
clearTimeout
.
Consulte o código completo: debounce.html
let timer;
button.addEventListener('click', () => {
score.incrementAndUpdateUI();
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
blockFor(1000);
}, 1000);
});
Rastreamento de desempenho
Apesar dos vários cliques, apenas uma tarefa blockFor
acaba em execução, aguardando até que não haja nenhum clique por um segundo antes de ser executada. Para interações que ocorrem em sequência, como digitar uma entrada de texto ou itens de destino 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á a chance azarada de que um novo clique ocorra logo após o período de debounce, cai no meio dessa longa tarefa e se torna uma interação muito lenta devido ao atraso de entrada.
Idealmente, se uma interação vier no meio da tarefa, queremos pausar nosso trabalho ocupado para que novas interações sejam tratadas imediatamente. Como podemos fazer isso?
Existem algumas APIs como a isInputPending
, mas geralmente é melhor dividir tarefas longas em partes.
Muitos setTimeout
s
Primeira tentativa: fazer algo simples.
Consulte 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 agende cada tarefa individualmente, e a entrada pode ter maior prioridade!
Voltamos a cinco segundos completos 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 superior a 100 milissegundos. O navegador prioriza os listeners de eventos recebidos em relação ao trabalho de setTimeout
, e as interações permanecem responsivas.
Essa estratégia funciona especialmente bem ao programar pontos de entrada separados, por exemplo, quando você precisa chamar vários recursos independentes no tempo de carregamento do aplicativo. Por padrão, apenas carregar scripts e executar tudo no momento de avaliação do script pode executar tudo em uma tarefa gigante e longa.
No entanto, essa estratégia não funciona tão bem para separar códigos com acoplamento rígido, como uma repetição for
que usa o estado compartilhado.
Agora com yield()
No entanto, podemos usar async
e await
modernos para adicionar facilmente "pontos de rendimento" a qualquer função JavaScript.
Exemplo:
Consulte o código completo: rendimentoy.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 é gerada após um pedaço de trabalho, e o navegador é capaz de responder a qualquer interação recebida, mas agora tudo o que é necessário é uma await schedulerDotYield()
em vez de setTimeout
s separadas, o que a torna ergonômica o suficiente para ser usada mesmo no meio de uma repetição 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 dedução, 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 a repetição blockInPiecesYieldyAborty
for
, fazendo o que for necessário, produzindo periodicamente a linha de execução principal para que o navegador permaneça responsivo a novas interações.
Quando um segundo clique ocorre, 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 execução novamente, ele percebe que signal.aborted
agora é true
e retorna imediatamente sem fazer mais trabalho.
18. Conclusão
Dividir todas as tarefas longas permite que um site seja responsivo a novas interações. Isso permite fornecer feedback inicial rapidamente e também permite que você tome decisões, como cancelar um trabalho em andamento. Às vezes, isso significa agendar pontos de entrada como tarefas separadas. Às vezes, isso significa adicionar "rendimento" onde for conveniente.
Lembrete
- O INP mede todas as interações.
- Cada interação é medida da entrada até a próxima pintura, a forma como o usuário vê a capacidade de resposta.
- Atrasos na entrada, duração do processamento de eventos e atrasos na apresentação afetam a capacidade de resposta da interação.
- Você pode medir o INP e os detalhamentos das interações com o DevTools com facilidade.
Estratégias
- Não têm códigos de longa duração (tarefas longas) nas suas páginas.
- Remova códigos desnecessários dos listeners de eventos para depois da próxima gravação.
- Certifique-se de que a atualização de renderização em si seja eficiente para o navegador.