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

1. Introdução

Este é um codelab interativo para aprender a medir a Interação com a Next Paint (INP) usando a biblioteca web-vitals.

Pré-requisitos

O que você vai aprender

  • Como adicionar a biblioteca web-vitals à 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 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.

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

Testar a página

Este codelab usa o Gastropodicon, um conhecido site de referência de anatomia do caracol, para explorar possíveis problemas com o INP.

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

Tente interagir com a página para saber quais interações são lentas.

3. Como instalar 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.

Neste codelab, vamos usar o painel Desempenho e o Console. É possível alternar entre elas nas guias na parte superior do DevTools a qualquer momento.

  • Os problemas de INP geralmente ocorrem em dispositivos móveis. Por isso, mude para a emulação de tela do dispositivo móvel.
  • Se estiver testando em um desktop ou laptop, o desempenho provavelmente será significativamente melhor do que em um dispositivo móvel real. Para ter uma visão mais realista do desempenho, clique no ícone de engrenagem no canto superior direito do painel Desempenho e selecione Lentidão da CPU 4x.

Uma captura de tela do painel "Performance" do DevTools ao lado do app, com a lentidão de CPU 4x selecionada

4. Instalação web-vitals

A web-vitals é uma biblioteca JavaScript usada para avaliar as métricas das Métricas da Web usadas para os usuários. Você pode usar a biblioteca para capturar esses valores e, em seguida, transmiti-los a um endpoint de análise para análise posterior, para nossos objetivos descobrirem quando e onde ocorrem interações lentas.

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

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

Você pode usar duas versões de web-vitals:

  • O padrão build deverá ser usado se você quiser rastrear os valores das Core Web Vitals no carregamento de uma página.
  • A atribuição build adiciona informações de depuração extras a cada métrica para diagnosticar por que uma métrica ficou com o valor correspondente.

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

Adicione web-vitals ao devDependencies do projeto executando npm install -D web-vitals.

Adicione 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

Tente interagir com a página novamente com o console aberto. Enquanto você clica na página, nada é registrado.

O INP é medido ao longo de todo o ciclo de vida de uma página. Por isso, por padrão, o web-vitals não informa INP até que o usuário saia ou feche a página. Esse é o comportamento ideal para beacons para algo como análise, mas não é ideal para depuração interativa.

web-vitals fornece uma opção 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 as anteriores, elas são informadas.

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 serão informadas ao console, atualizando sempre que houver uma nova versão mais lenta. Por exemplo, tente digitar na caixa de pesquisa e depois excluir a entrada.

Uma captura de tela do console do DevTools com mensagens INP impressas

5. O que compõe uma atribuição?

Vamos começar com a primeira interação que a maioria dos usuários vai 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. É o que acontece aqui.

Clique em Yes para aceitar (demonstração) cookies e confira os dados de 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 momento em que o usuário clicou até a próxima pintura foi de 344 milissegundos, um "precisa de melhorias" INP. A matriz entries tem todos os valores PerformanceEntry associados a essa interação. Neste caso, apenas um evento de clique.

No entanto, para saber o que está acontecendo nesse período, nosso maior interesse é a propriedade attribution. Para criar os dados de atribuição, o web-vitals descobre qual Long Animations Frame (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, estilo e layout de requestAnimationFrame.

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

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

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

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

Primeiro, há informações sobre com o que houve interação:

  • interactionTargetElement: uma referência em tempo real ao elemento com que a interação ocorreu (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 o momento em que o listener de eventos dessa interação começou a ser executado. Nesse caso, o atraso de entrada foi de apenas 27 milissegundos, mesmo com a limitação de CPU ativada.
  • processingDuration: o tempo que leva para os listeners de eventos serem executados até a conclusão. Muitas vezes, as páginas têm vários listeners para um único evento (por exemplo, pointerdown, pointerup e click). Se todas elas forem executadas no mesmo frame de animação, elas serão reunidas nesse período. Nesse caso, a duração do processamento leva 295,6 milissegundos, a maior parte do tempo de INP.
  • presentationDelay: o tempo entre a conclusão dos listeners do evento e o momento em que o navegador terminar de pintar o próximo frame. Neste caso, 21,4 milissegundos.

Essas fases de INP podem ser um indicador vital para diagnosticar o que precisa ser otimizado. O guia de INP do Optimize tem mais informações sobre esse assunto.

Investigando um pouco mais, processedEventEntries contém cinco eventos, ao contrário do único evento da matriz entries de 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 INP. Nesse caso, um clique. Os 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 vital se eles também forem lentos, porque todos contribuíram para a lentidão na capacidade de resposta.

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 de animação longo.

longAnimationFrameEntries

Expansão da 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 detalhar o tempo gasto com estilização. O artigo da API Long Animation Frames se aprofunda nessas propriedades. No momento, estamos principalmente interessados na propriedade scripts, que contém entradas com 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 uma única event-listener, invocada em BUTTON#confirm.onclick. Podemos até mesmo ver o URL de origem do script e a posição do caractere em que a função foi definida.

Para viagem

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 a métrica total value).
  • O código lento do listener de eventos começa a partir de um listener de clique definido em third-party/cmp.js (de scripts.sourceURL).

Isso é suficiente para sabermos o que precisa ser otimizado.

6. Vários listeners de eventos

Atualize a página para que o console do DevTools fique limpo e a interação do 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 de alto nível 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: [...],
  }
}

O valor de INP é fraco (com a limitação de CPU ativada) devido à interação do teclado com o elemento input#search-terms. A maior parte do tempo (1.061 milissegundos de um INP total de 1.072 milissegundos) foi gasta na duração do processamento.

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

Troca frequente de layouts

A primeira entrada da matriz scripts nos oferece 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 do processamento ocorre durante a execução desse script, que é um listener input (o invocador é INPUT#search-terms.oninput). O nome da função é fornecido (handleSearch), assim como a posição dos caracteres dentro do arquivo de origem index.js.

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

Essa deve ser uma prioridade máxima a ser corrigida.

Listeners repetidos

Individualmente, não há nada de especial nas próximas duas 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
}]

As duas entradas são listeners keyup, executando uma logo após a outra. Os listeners são funções anônimas (portanto, nada é relatado 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, levando esse listener de eventos a ser executado duas vezes antes que qualquer coisa pudesse ser pintada.

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

Como essa é uma interação de pesquisa/preenchimento automático, desconsiderar a entrada é uma boa estratégia para processar no máximo uma tecla pressionada por frame.

7. Atraso na entrada

O motivo típico de 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á sendo carregada, e a linha de execução principal está ocupada fazendo o trabalho inicial de configuração do DOM, layout e estilização da página, além de avaliar e executar scripts.
  • A página geralmente está 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 as interações futuras, o que foi visto no último exemplo.

A página de demonstração tem um recurso secreto em que, se você clicar no logotipo do caracol na parte superior da página, ele vai começar a animar e fazer alguns trabalhos pesados de JavaScript da 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 inferior da rejeição e veja até que ponto de um INP você pode acionar.

Por exemplo, mesmo que você não acione nenhum outro listener de eventos, como ao clicar e focar a caixa de pesquisa logo que o caracol salta, o trabalho da linha de execução principal fará com que a página pare de responder por um período considerável.

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

Veja um exemplo de atribuição de focar somente a caixa de pesquisa durante o movimento da armadilha:

{
  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: [{...}]
    }]
  }
}

Conforme 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, tomando 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. Na verdade, era outra coisa na página que atrasou o início do processamento da interação. No entanto, como saberíamos por onde começar?

As entradas do script LoAF chegaram 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 tivesse nada a ver com a interação, ela desacelerou o frame da animação e, portanto, está incluída nos dados do LoAF unidos ao evento de interação.

A partir disso, podemos ver 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 nossos arquivos de origem.

8. Atraso na apresentação: quando uma atualização não é exibida

O atraso da apresentação mede o tempo desde que os listeners de eventos terminam a execução até o navegador conseguir exibir um novo frame na tela, mostrando feedback visível ao usuário.

Atualize a página para redefinir o valor de INP novamente e abra o menu de navegação. Há um problema quando ele abre.

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 da 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,
}]

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

9. Conclusão

Como agregar dados de campo

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

Por exemplo, é extremamente útil saber qual elemento de página é uma fonte comum de interações lentas. No entanto, caso sua página tenha nomes de classes CSS compilados que mudam de build para build, os seletores web-vitals do mesmo elemento podem ser diferentes entre os builds.

Em vez disso, pense no seu aplicativo específico para determinar o que é mais útil e como os dados podem ser agregados. Por exemplo, antes de fazer a beaconing dos 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 a meta cumpre.

Da mesma forma, as entradas scripts podem ter hashes baseados em arquivos nos caminhos sourceURL, o que dificulta a combinação, mas é possível remover os hashes com base no seu processo de build conhecido antes de fazer o beaconing dos dados.

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 qualquer lugar

A atribuição INP baseada em LoAF é um auxiliar de depuração poderoso. Ele 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 os dados de atribuição da 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 abaixo no console do DevTools para conferir o que você encontra:

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