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

1. Introdução

Este é um codelab interativo para aprender a medir a 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 melhorar a INP.

O que é necessário

  • Um computador com 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.

  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 de anatomia de caracóis) para analisar possíveis problemas com a INP.

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 se situar 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 das Ferramentas do desenvolvedor a qualquer momento.

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

Uma captura de tela do painel de desempenho do DevTools ao lado do app, com a opção de 4x redução da CPU selecionada

4. Como instalar web-vitals

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

várias maneiras de adicionar a biblioteca a uma página. A maneira de instalar a biblioteca no seu site depende de como você gerencia dependências, o processo de build e outros fatores. Confira os documentos da biblioteca para conferir todas as opções.

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

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

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

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. 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 beacons em algo como análises, mas não é ideal para depuração interativa.

web-vitals oferece a opção reportAllChanges para relatórios mais detalhados. Quando ativado, nem todas as interações são informadas, mas sempre que há uma interação mais lenta do que qualquer outra 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 vão ser informadas ao console, sendo atualizadas sempre que houver uma nova interação mais lenta. Por exemplo, tente digitar na caixa de pesquisa e excluir a entrada.

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

5. O que está 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. É o que acontece aqui.

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

Objeto de dados de INP registrado no console do DevTools

Essas informações de nível superior estão disponíveis nos builds de Web Vitals padrão e de atribuição:

{
  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 pintura foi de 344 milissegundos, uma INP"precisa de melhorias". A matriz entries tem todos os valores PerformanceEntry associados a essa interação. Neste caso, apenas um evento de clique.

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

Abra a propriedade attribution para conferir mais informações. Os dados são muito mais ricos.

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 em tempo real ao elemento com que houve interação (se ele não tiver sido removido do DOM).
  • interactionTarget: um seletor para encontrar o elemento na página.

Em seguida, o tempo é dividido em um nível alto:

  • inputDelay: o tempo entre o início da interação do usuário (por exemplo, o clique do mouse) e o início da execução do listener de eventos dessa interação. Nesse caso, o atraso de entrada foi de apenas 27 milissegundos, mesmo com o throttling da CPU ativado.
  • processingDuration: o tempo que os listeners de eventos levam para 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 todos forem executados no mesmo frame de animação, eles serão agrupados nesse momento. 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 de eventos e o término da pintura do próximo frame pelo navegador. Nesse caso, 21,4 milissegundos.

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

Analisando um pouco mais, o processedEventEntries contém cinco eventos, em vez do único evento na matriz entries de nível superior do INP. 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, neste caso, um clique. A atribuição processedEventEntries são todos os eventos que foram processados durante o mesmo frame. Ele inclui outros eventos, como mouseover e mousedown, e não apenas o evento de clique. Conhecer esses outros eventos pode ser vital se eles também forem lentos, já que todos contribuíram para a lentidão da resposta.

Por fim, há a matriz longAnimationFrameEntries. Pode ser uma entrada única, 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

Como expandir a entrada de 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 aborda essas propriedades em 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 execução longa:

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. É possível até mesmo conferir o URL da origem do script e a posição do caractere em que a função foi definida.

Comida para viagem

O que pode ser determinado sobre esse caso com esses 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 do listener de eventos lentos começa com um listener de clique definido em third-party/cmp.js (a partir 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 seja limpo e a interação de consentimento do cookie não seja mais a 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, um exame 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: [...],
  }
}

É um valor de INP ruim (com restrição de CPU ativada) de uma 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 gasto no processamento.

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

Troca frequente de layout

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

No entanto, há uma nova propriedade: forcedStyleAndLayoutDuration. Esse foi o tempo gasto nessa invocação de 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 em thrashing de layout.

Isso precisa ser corrigido com prioridade.

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

Ambas as entradas são listeners keyup, que são executadas 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 estranho é que ambos vêm do mesmo arquivo de origem e da mesma posição do caractere.

O navegador acabou processando vários pressionamentos de tecla em um único frame de animação, fazendo com que esse listener de eventos fosse 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 levam para serem concluídos, mais eventos de entrada adicionais podem ser recebidos, estendendo a interação lenta por muito mais tempo.

Como essa é uma interação de pesquisa/autopreenchimento, a debouncing da entrada seria uma boa estratégia para que, no máximo, um pressionamento de tecla seja processado por frame.

7. Atraso na entrada

O motivo mais comum 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á sendo carregada e a linha de execução principal está ocupada fazendo o trabalho inicial de configuração do DOM, disposição e estilo 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 demoram tanto para serem processadas que atrasam as interações futuras, como 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 de cima da página, ele vai começar a animar e fazer um trabalho pesado de JavaScript na linha de execução principal.

  • Clique no logotipo do caracol para iniciar a animação.
  • As tarefas do 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 da taxa de rejeição e veja qual é o nível máximo 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 pula, o trabalho da linha de execução principal faz com que a página não responda por um tempo considerável.

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

Confira um exemplo de atribuição de focar apenas a caixa de pesquisa durante a saída lenta:

{
  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. Mesmo sabendo 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 outra coisa na página que atrasou o início do processamento da interação, mas como saberíamos por 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 o frame da animação e, portanto, foi incluída nos dados de LoAF que foram mesclados ao evento de interação.

Com isso, 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 arquivos de origem.

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

O atraso de apresentação mede o tempo entre o término da execução dos listeners de eventos e o momento em que o navegador consegue pintar um novo frame na tela, mostrando o feedback visível ao usuário.

Atualize a página para redefinir o valor do INP novamente e abra o menu de navegação. Há um problema ao abrir.

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 é o principal responsável pela interação lenta. Isso significa que o que quer que esteja 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. Dessa vez, o atraso na apresentação é causado por um callback requestAnimationFrame.

9. Conclusão

Como agregar dados de campo

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

Por exemplo, é extremamente útil saber qual elemento da 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 podem ser diferentes entre os builds.

Em vez disso, você precisa pensar na sua aplicação específica para determinar o que é mais útil e como os dados podem ser agregados. Por exemplo, antes de enviar os dados de atribuição do beacon, você pode substituir o seletor web-vitals por um identificador próprio, com base no componente em que o alvo está ou nas funções ARIA que ele cumpre.

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 seu 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 usando um subconjunto deles, é mais valioso do que não ter dados 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 poderosa de depuração. Ele oferece dados granulares sobre o que aconteceu especificamente durante um INP. Em muitos casos, ele pode apontar o local exato em um script em que você deve começar a otimização.

Agora você já pode usar os 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 saber 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