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.
  • A página está geralmente ocupada, por exemplo, executando cálculos, animações baseadas em script ou anúncios.
  • As interações anteriores levam tanto tempo para serem processadas que atrasam interações futuras, o que foi visto no exemplo anterior.

A página de demonstração tem um recurso secreto em que, se você clicar no logotipo do caracol na parte de cima da página, ele começará a animar e a fazer um trabalho pesado de JavaScript na linha de execução principal.

  • Clique no logotipo do caracol para iniciar a animação.
  • As tarefas JavaScript são acionadas quando o caracol está na parte de baixo do salto. Tente interagir com a página o mais próximo possível da parte de baixo do salto e confira o nível de INP que você pode acionar.

Por exemplo, mesmo que você não acione outros listeners de eventos, como clicar e focar a caixa de pesquisa assim que o caracol saltar, o trabalho da linha de execução principal fará com que a página fique sem resposta por um período considerável.

Em muitas páginas, o trabalho pesado da linha de execução principal não será tão bem-comportado, mas essa é uma boa demonstração para conferir como ele pode ser identificado nos dados de atribuição da INP.

Confira um exemplo de atribuição de apenas focar a caixa de pesquisa durante o salto do caracol:

{
  name: 'INP',
  value: 728,
  rating: 'poor',

  attribution: {
    interactionTargetElement: Element,
    interactionTarget: '#search-terms',
    interactionType: 'pointer',

    inputDelay: 702.3,
    processingDuration: 4.9,
    presentationDelay: 20.8,

    longAnimationFrameEntries: [{
      name: 'long-animation-frame',
      startTime: 2064.8,
      duration: 790,

      renderStart: 2065,
      styleAndLayoutStart: 2854.2,
      firstUIEventTimestamp: 0,
      blockingDuration: 740,

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

Como previsto, os listeners de eventos foram executados rapidamente, mostrando uma duração de processamento de 4,9 milissegundos, e a grande maioria da interação ruim foi gasta no atraso de entrada, levando 702,3 milissegundos de um total de 728.

Essa situação pode ser difícil de depurar. Embora saibamos com o que o usuário interagiu e como, também sabemos que essa parte da interação foi concluída rapidamente e não foi um problema. Em vez disso, foi algo mais na página que atrasou o início do processamento da interação, mas como saberíamos onde começar a procurar?

As entradas de script do LoAF estão aqui para salvar o dia:

scripts: [{
  name: 'script',
  invoker: 'SPAN.onanimationiteration',
  invokerType: 'event-listener',

  startTime: 2065,
  executionStart: 2065,
  duration: 788,

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

Embora essa função não tenha nada a ver com a interação, ela diminuiu a velocidade do frame de animação e, portanto, está incluída nos dados do LoAF que são unidos ao evento de interação.

Com isso, podemos conferir como a função que atrasou o processamento da interação foi acionada (por um listener animationiteration), exatamente qual função foi responsável e onde ela estava localizada nos arquivos de origem.

8. Atraso de apresentação: quando uma atualização não é renderizada

O atraso de apresentação mede o tempo desde a conclusão dos listeners de eventos até o momento em que o navegador pode renderizar um novo frame na tela, mostrando ao usuário um feedback visível.

Atualize a página para redefinir o valor da INP novamente e abra o menu de hambúrguer. Há um problema definido quando ele é aberto.

Como ficou isso aqui?

{
  name: 'INP',
  value: 376,
  rating: 'needs-improvement',
  delta: 352,

  attribution: {
    interactionTarget: '#sidenav-button>svg',
    interactionType: 'pointer',

    inputDelay: 12.8,
    processingDuration: 14.7,
    presentationDelay: 348.5,

    longAnimationFrameEntries: [{
      name: 'long-animation-frame',
      startTime: 651,
      duration: 365,

      renderStart: 673.2,
      styleAndLayoutStart: 1004.3,
      firstUIEventTimestamp: 138.6,
      blockingDuration: 315,

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

Desta vez, é o atraso de apresentação que compõe a maior parte da interação lenta. Isso significa que o que está bloqueando a linha de execução principal ocorre depois que os listeners de eventos são concluídos.

scripts: [{
  entryType: 'script',
  invoker: 'FrameRequestCallback',
  invokerType: 'user-callback',

  startTime: 673.8,
  executionStart: 673.8,
  duration: 330,

  sourceURL: 'http://localhost:8080/js/side-nav.js',
  sourceFunctionName: '',
  sourceCharPosition: 1193,
}]

Ao analisar a única entrada na matriz scripts, vemos que o tempo é gasto em um user-callback de um FrameRequestCallback. Desta vez, o atraso de apresentação é causado por um requestAnimationFrame callback.

9. Conclusão

Como agregar dados de campo

É importante reconhecer que tudo isso é mais fácil ao analisar uma única entrada de atribuição de INP de um único carregamento de página. Como esses dados podem ser agregados para depurar a INP com base em dados de campo? A quantidade de detalhes úteis realmente dificulta isso.

Por exemplo, é extremamente útil saber qual elemento de página é uma fonte comum de interações lentas. No entanto, se a página tiver nomes de classe CSS compilados que mudam de build para build, os seletores web-vitals do mesmo elemento poderão ser diferentes entre builds.

Em vez disso, você precisa pensar no seu aplicativo específico para determinar o que é mais útil e como os dados podem ser agregados. Por exemplo, antes de enviar dados de atribuição de volta, você pode substituir o seletor web-vitals por um identificador próprio, com base no componente em que o destino está ou nas funções ARIA que o destino atende.

Da mesma forma, as entradas scripts podem ter hashes baseados em arquivos nos caminhos sourceURL que dificultam a combinação, mas você pode remover os hashes com base no processo de build conhecido antes de enviar os dados de volta.

Infelizmente, não há um caminho fácil com dados tão complexos, mas mesmo usar um subconjunto deles é mais valioso do que nenhum dado de atribuição para o processo de depuração.

Atribuição em todos os lugares

A atribuição de INP baseada em LoAF é uma ferramenta de depuração poderosa. Ela oferece dados granulares sobre o que aconteceu especificamente durante uma INP. Em muitos casos, ela pode indicar o local exato em um script em que você deve iniciar seus esforços de otimização.

Agora você está pronto para usar dados de atribuição de INP em qualquer site.

Mesmo que você não tenha acesso para editar uma página, é possível recriar o processo deste codelab executando o snippet a seguir no console do DevTools para conferir o que você pode encontrar:

const script = document.createElement('script');
script.src = 'https://unpkg.com/web-vitals@4/dist/web-vitals.attribution.iife.js';
script.onload = function () {
  webVitals.onINP(console.log, {reportAllChanges: true});
};
document.head.appendChild(script);

Saiba mais