Medir a interação com a próxima pintura (INP)

1. Introdução

Este é um codelab interativo para aprender a medir a métrica Interaction to Next Paint (INP) usando a biblioteca web-vitals.

Pré-requisitos

O que você vai aprender

  • Como adicionar a biblioteca web-vitals à sua página e usar os dados de atribuição dela.
  • Use os dados de atribuição para diagnosticar onde e como começar a melhorar a INP.

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

Receber e executar o código

O código está no o web-vitals-codelabs repositório.

  1. Clone o repositório no terminal: git clone https://github.com/GoogleChromeLabs/web-vitals-codelabs.git.
  2. Acesse o diretório clonado: cd web-vitals-codelabs/measuring-inp.
  3. Instale as dependências: npm ci.
  4. Inicie o servidor da Web: npm run start.
  5. Acesse http://localhost:8080/ no navegador.

Testar a página

Este codelab usa o Gastropodicon (um site de referência popular sobre anatomia de caracóis) para explorar possíveis problemas com a INP.

Captura de tela da página de demonstração do Gastropodicon

Interaja com a página para ter uma ideia de quais interações são lentas.

3. Como se orientar no 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 de teclado.

Neste codelab, vamos usar o painel Performance e o Console. É possível alternar entre eles nas guias na parte de cima do DevTools a qualquer momento.

  • Os problemas de INP geralmente acontecem em dispositivos móveis. Por isso, mude para a emulação de exibição para dispositivos móveis.
  • Se você estiver testando em um computador desktop ou laptop, a performance provavelmente será significativamente melhor do que em um dispositivo móvel real. Para uma visão mais realista da performance, clique na engrenagem no canto superior direito do painel Performance e selecione CPU 4x slowdown.

Captura de tela do painel "Performance" das DevTools ao lado do app, com a opção "4x CPU slowdown" selecionada

4. Como instalar o web-vitals

web-vitals é uma biblioteca JavaScript para medir as métricas das Core Web Vitals que os usuários encontram. Você pode usar a biblioteca para capturar esses valores e, em seguida, enviá-los a um endpoint de análise para análise posterior, com o objetivo de descobrir quando e onde ocorrem interações lentas.

Há algumas maneiras diferentes de adicionar a biblioteca a uma página. A forma de instalar a biblioteca no seu site depende de como você gerencia dependências, o processo de build e outros fatores. Consulte a documentação da biblioteca para conferir todas as opções.

Este codelab vai instalar do npm e carregar o script diretamente para evitar aprofundar um processo de build específico.

Há duas versões do web-vitals que você pode usar:

  • O build "padrão" deve ser usado se você quiser acompanhar os valores das métricas das Core Web Vitals em um carregamento de página.
  • O build "atribuição" adiciona informações de depuração extras a cada métrica para diagnosticar por que uma métrica acaba com o valor que tem.

Para medir a INP neste codelab, queremos o build de atribuição.

Adicione web-vitals às devDependencies do projeto executando npm install -D web-vitals

Adicionar web-vitals à página:

Adicione a versão de atribuição do script à parte de baixo de index.html e registre os resultados no console:

<script type="module">
  import {onINP} from './node_modules/web-vitals/dist/web-vitals.attribution.js';

  onINP(console.log);
</script>

Faça um teste

Interaja com a página novamente com o console aberto. Ao clicar na página, nada é registrado.

A INP é medida durante todo o ciclo de vida de uma página. Por padrão, web-vitals não informa a INP até que o usuário saia ou feche a página. Esse é o comportamento ideal para o beaconing de algo como a análise, mas é menos ideal para a depuração interativa.

web-vitals oferece uma opção the reportAllChanges para relatórios mais detalhados. Quando ativada, nem todas as interações são informadas, mas sempre que há uma interação mais lenta do que qualquer uma anterior, ela é informada.

Tente adicionar a opção ao script e interagir com a página novamente:

<script type="module">
  import {onINP} from './node_modules/web-vitals/dist/web-vitals.attribution.js';

  onINP(console.log, {reportAllChanges: true});
</script>

Atualize a página e as interações agora serão informadas ao console, atualizando sempre que houver uma nova mais lenta. Por exemplo, tente digitar na caixa de pesquisa e excluir a entrada.

Captura de tela do console do DevTools com mensagens INP impressas com sucesso

5. O que há em uma atribuição?

Vamos começar com a primeira interação que a maioria dos usuários terá com a página, a caixa de diálogo de consentimento de cookies.

Muitas páginas têm scripts que precisam de cookies acionados de forma síncrona quando os cookies são aceitos por um usuário, fazendo com que o clique se torne uma interação lenta. É isso que acontece aqui.

Clique em Sim para aceitar os cookies (demonstração) e confira os dados da INP agora registrados no console do DevTools.

Objeto de dados INP registrado no console do DevTools

Essas informações de nível superior estão disponíveis nos builds padrão e de atribuição do web-vitals:

{
  name: 'INP',
  value: 344,
  rating: 'needs-improvement',
  entries: [...],
  id: 'v4-1715732159298-8028729544485',
  navigationType: 'reload',
  attribution: {...},
}

O tempo decorrido desde o clique do usuário até a próxima exibição foi de 344 milissegundos, uma "precisa de melhorias" INP. A matriz entries tem todos os valores PerformanceEntry associados a essa interação. Nesse caso, apenas um evento de clique.

No entanto, para descobrir o que está acontecendo durante esse período, estamos mais interessados na propriedade attribution. Para criar os dados de atribuição, web-vitals encontra qual frame de animações longas (LoAF) se sobrepõe ao evento de clique. O LoAF pode fornecer dados detalhados sobre como o tempo foi gasto durante esse frame, desde os scripts executados até o tempo gasto em um callback requestAnimationFrame, estilo e layout.

Expanda a propriedade attribution para conferir mais informações. Os dados são muito mais avançados.

attribution: {
  interactionTargetElement: Element,
  interactionTarget: '#confirm',
  interactionType: 'pointer',

  inputDelay: 27,
  processingDuration: 295.6,
  presentationDelay: 21.4,

  processedEventEntries: [...],
  longAnimationFrameEntries: [...],
}

Primeiro, há informações sobre o que foi interagido:

  • interactionTargetElement: uma referência ativa ao elemento com que foi interagido (se o elemento não tiver sido removido do DOM).
  • interactionTarget: um seletor para encontrar o elemento na página.

Em seguida, o tempo é dividido de maneira geral:

  • inputDelay: o tempo entre o momento em que o usuário iniciou a interação (por exemplo, clicou no mouse) e quando o listener de eventos para essa interação começou a ser executado. Nesse caso, o atraso de entrada foi de apenas 27 milissegundos, mesmo com a limitação da CPU ativada.
  • processingDuration: o tempo que os listeners de eventos levam para serem concluídos. Muitas vezes, as páginas têm vários listeners para um único evento (por exemplo, pointerdown, pointerup e click). Se todos forem executados no mesmo frame de animação, eles serão unidos nesse período. Nesse caso, a duração do processamento leva 295,6 milissegundos, a maior parte do tempo da INP.
  • presentationDelay: o tempo desde a conclusão dos listeners de eventos até o momento em que o navegador termina de pintar o próximo frame. Nesse caso, 21,4 milissegundos.

Essas fases da INP podem ser um sinal essencial para diagnosticar o que precisa ser otimizado. O guia Otimizar a INP tem mais informações sobre esse assunto.

Aprofundando um pouco mais, o processedEventEntries contém cinco eventos, em vez do único evento na matriz entries da INP de nível superior. Qual é a diferença?

processedEventEntries: [
  {
    name: 'mouseover',
    entryType: 'event',
    startTime: 1801.6,
    duration: 344,
    processingStart: 1825.3,
    processingEnd: 1825.3,
    cancelable: true
  },
  {
    name: 'mousedown',
    entryType: 'event',
    startTime: 1801.6,
    duration: 344,
    processingStart: 1825.3,
    processingEnd: 1825.3,
    cancelable: true
  },
  {name: 'mousedown', ...},
  {name: 'mouseup', ...},
  {name: 'click', ...},
],

A entrada de nível superior é o evento da INP, nesse caso, um clique. As processedEventEntries de atribuição são todos os eventos processados durante o mesmo frame. Observe que ele inclui outros eventos, como mouseover e mousedown, não apenas o evento de clique. Saber sobre esses outros eventos pode ser essencial se eles também forem lentos, já que todos contribuíram para a resposta lenta.

Por fim, há a matriz longAnimationFrameEntries. Essa pode ser uma única entrada, mas há casos em que uma interação pode se espalhar por vários frames. Aqui temos o caso mais simples com um único frame longo de animação.

longAnimationFrameEntries

Como expandir a entrada do LoAF:

longAnimationFrameEntries: [{
  name: 'long-animation-frame',
  startTime: 1823,
  duration: 319,

  renderStart: 2139.5,
  styleAndLayoutStart: 2139.7,
  firstUIEventTimestamp: 1801.6,
  blockingDuration: 268,

  scripts: [{...}]
}],

Há vários valores úteis aqui, como a quantidade de tempo gasto no estilo. O artigo da API Long Animation Frames (link em inglês) aborda essas propriedades com mais detalhes. No momento, estamos interessados principalmente na propriedade scripts, que contém entradas que fornecem detalhes sobre os scripts responsáveis pelo frame de longa duração:

scripts: [{
  name: 'script',
  invoker: 'BUTTON#confirm.onclick',
  invokerType: 'event-listener',

  startTime: 1828.6,
  executionStart: 1828.6,
  duration: 294,

  sourceURL: 'http://localhost:8080/third-party/cmp.js',
  sourceFunctionName: '',
  sourceCharPosition: 1144
}]

Nesse caso, podemos dizer que o tempo foi gasto principalmente em um único event-listener, invocado em BUTTON#confirm.onclick. Podemos até mesmo conferir o URL da origem do script e a posição do caractere de onde a função foi definida.

Conclusão

O que pode ser determinado sobre esse caso com base nesses dados de atribuição?

  • A interação foi acionada por um clique no elemento button#confirm (de attribution.interactionTarget e da propriedade invoker em uma entrada de atribuição de script).
  • O tempo foi gasto principalmente na execução de listeners de eventos (de attribution.processingDuration em comparação com o value total da métrica).
  • O código do listener de eventos lento começa com um listener de cliques definido em third-party/cmp.js (de scripts.sourceURL).

Esses dados são suficientes para saber onde precisamos otimizar.

6. Vários listeners de eventos

Atualize a página para que o console do DevTools fique limpo e a interação de consentimento de cookies não seja mais a interação mais longa.

Comece a digitar na caixa de pesquisa. O que os dados de atribuição mostram? O que você acha que está acontecendo?

Dados de atribuição

Primeiro, uma verificação geral de um exemplo de teste da demonstração:

{
  name: 'INP',
  value: 1072,
  rating: 'poor',
  attribution: {
    interactionTargetElement: Element,
    interactionTarget: '#search-terms',
    interactionType: 'keyboard',

    inputDelay: 3.3,
    processingDuration: 1060.6,
    presentationDelay: 8.1,

    processedEventEntries: [...],
    longAnimationFrameEntries: [...],
  }
}

É um valor de INP ruim (com a limitação da CPU ativada) de uma interação de teclado com o elemento input#search-terms. A maior parte do tempo (1.061 milissegundos de um total de 1.072 milissegundos de INP) foi gasto na duração do processamento.

No entanto, as entradas scripts são mais interessantes.

Layout thrashing

A primeira entrada da matriz scripts nos fornece um contexto valioso:

scripts: [{
  name: 'script',
  invoker: 'BUTTON#confirm.onclick',
  invokerType: 'event-listener',

  startTime: 4875.6,
  executionStart: 4875.6,
  duration: 497,
  forcedStyleAndLayoutDuration: 388,

  sourceURL: 'http://localhost:8080/js/index.js',
  sourceFunctionName: 'handleSearch',
  sourceCharPosition: 940
},
...]

A maior parte da duração do processamento ocorre durante a execução desse script, que é um input listener (o invocador é INPUT#search-terms.oninput). O nome da função é fornecido (handleSearch), assim como a posição do caractere dentro do arquivo de origem index.js.

No entanto, há uma nova propriedade: forcedStyleAndLayoutDuration. Esse foi o tempo gasto na invocação do script em que o navegador foi forçado a refazer o layout da página. Em outras palavras, 78% do tempo (388 milissegundos de 497) gasto na execução desse listener de eventos foi gasto no layout thrashing.

Essa deve ser uma prioridade máxima para corrigir.

Listeners repetidos

Individualmente, não há nada de especial nas duas próximas entradas de script:

scripts: [...,
{
  name: 'script',
  invoker: '#document.onkeyup',
  invokerType: 'event-listener',

  startTime: 5375.3,
  executionStart: 5375.3,
  duration: 124,

  sourceURL: 'http://localhost:8080/js/index.js',
  sourceFunctionName: '',
  sourceCharPosition: 1526,
},
{
  name: 'script',
  invoker: '#document.onkeyup',
  invokerType: 'event-listener',

  startTime: 5673.9,
  executionStart: 5673.9,
  duration: 95,

  sourceURL: 'http://localhost:8080/js/index.js',
  sourceFunctionName: '',
  sourceCharPosition: 1526
}]

Ambas as entradas são listeners keyup, executando uma após a outra. Os listeners são funções anônimas (portanto, nada é informado na propriedade sourceFunctionName), mas ainda temos um arquivo de origem e uma posição de caractere, para que possamos encontrar onde o código está.

O que é estranho é que ambos são do mesmo arquivo de origem e posição de caractere.

O navegador acabou processando várias teclas pressionadas em um único frame de animação, fazendo com que esse listener de eventos fosse executado duas vezes antes que algo pudesse ser renderizado.

Esse efeito também pode ser composto, em que quanto mais tempo os listeners de eventos levarem para serem concluídos, mais eventos de entrada adicionais poderão chegar, estendendo a interação lenta por muito mais tempo.

Como essa é uma interação de pesquisa/preenchimento automático, a eliminação de duplicação da entrada seria uma boa estratégia para que, no máximo, uma tecla pressionada seja processada por frame.

7. Latência na entrada

O motivo típico para atrasos de entrada (o tempo entre a interação do usuário e o momento em que um listener de eventos pode começar a processar a interação) é porque a linha de execução principal está ocupada. Isso pode ter várias causas:

  • A página está carregando e a linha de execução principal está ocupada fazendo o trabalho inicial de configuração do DOM, layout e estilo da página, além de avaliar e executar scripts.