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

1. Introdução

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

Teste 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 o INP.

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

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

3. Como se orientar no Chrome DevTools

Abra o DevTools no menu Mais ferramentas > Ferramentas para desenvolvedores, clique com o botão direito do mouse na página e selecione Inspecionar ou use 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 tela móvel.
  • Se você estiver testando em um computador ou laptop, o desempenho provavelmente será muito melhor do que em um dispositivo móvel real. Para ter uma visão mais realista da performance, clique na engrenagem no canto superior direito do painel Performance e selecione Redução de velocidade da CPU em 4x.

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

4. Como instalar web-vitals

O web-vitals é uma biblioteca JavaScript para medir as métricas das Web Vitals que seus usuários experimentam. Você pode usar a biblioteca para capturar esses valores e, em seguida, enviar beacons para um endpoint de análise para análise posterior. Para nossos fins, isso ajuda a descobrir quando e onde ocorrem interações lentas.

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. Confira a documentação da biblioteca para conhecer todas as opções.

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

Há duas versões de web-vitals que podem ser usadas:

  • A build "padrão" deve ser usada se você quiser rastrear os valores das métricas das Core Web Vitals em um carregamento de página.
  • O build "attribution" adiciona mais informações de depuração a cada métrica para diagnosticar por que ela tem 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, 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 análises, mas não é tão bom para depuração interativa.

O web-vitals oferece 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 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. Agora as interações serão informadas ao console e atualizadas sempre que houver uma nova interação mais lenta. Por exemplo, digite na caixa de pesquisa e exclua 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 tem com a página: a caixa de diálogo de consentimento para o uso de cookies.

Muitas páginas têm scripts que precisam de cookies acionados de forma síncrona quando eles 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 de demonstração e confira os dados de INP 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 nas versões padrão e de atribuição das 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 renderização foi de 344 milissegundos, um INP"precisa de melhoria". A matriz entries tem todos os valores PerformanceEntry associados a essa interação. Nesse caso, apenas um evento de clique.

Para saber o que está acontecendo durante esse período, o mais interessante é a propriedade attribution. Para criar os dados de atribuição, o web-vitals encontra qual frame de animações longas (LoAF, na sigla em inglês) se sobrepõe ao evento de clique. Em seguida, 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 ver 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 houve interação (se ele não foi 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 com o 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 redução da CPU ativada.
  • processingDuration: o tempo que os listeners de eventos levam para ser 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 unidos 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 de eventos e a finalização da pintura do próximo frame pelo navegador. Nesse caso, 21,4 milissegundos.

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

Analisando um pouco mais a fundo, o processedEventEntries contém cinco eventos, em vez do único evento na matriz entries do 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 the INP, neste caso, um clique. A atribuição processedEventEntries são todos os eventos processados durante o mesmo frame. 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 estiverem lentos, já que todos contribuíram para a lentidão da 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 longo de animação.

longAnimationFrameEntries

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 na estilização. O artigo sobre a API Long Animation Frames aborda essas propriedades com mais detalhes. No momento, estamos interessados principalmente 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 um único event-listener, invocado em BUTTON#confirm.onclick. Podemos até ver o URL de 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 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 executando listeners de eventos (de attribution.processingDuration em comparação com a métrica total value).
  • 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 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: [...],
  }
}

É um valor de INP ruim (com limitaçã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 total de 1.072 milissegundos de INP) foi gasta na duração do processamento.

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

Sobrecarga de layout

A primeira entrada da matriz scripts 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 da duração 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 do caractere dentro do arquivo de origem index.js.

No entanto, há uma nova propriedade: forcedStyleAndLayoutDuration. Foi o tempo gasto nessa invocação de script em que o navegador foi forçado a reestruturar a página. Em outras palavras, 78% do tempo (388 milissegundos de 497) gasto executando esse listener de eventos foi, na verdade, gasto em layout thrashing.

Isso deve ser uma prioridade máxima para correção.

Ouvintes recorrentes

Individualmente, não há nada de especialmente notável 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
}]

As duas entradas são listeners keyup, executando uma após a outra. Os listeners são funções anônimas. Por isso, nada é informado na propriedade sourceFunctionName. No entanto, ainda temos um arquivo de origem e uma posição de caractere, o que nos permite encontrar o código.

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 se agravar. Quanto mais tempo os listeners de eventos levam para serem concluídos, mais eventos de entrada adicionais podem chegar, prolongando a interação lenta por muito mais tempo.

Como essa é uma interação de pesquisa/preenchimento automático, a remoção de duplicação da entrada seria uma boa estratégia para que, no máximo, uma tecla 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 início do processamento da interação por um listener de eventos) é que a linha de execução principal está ocupada. Isso pode acontecer por vários motivos:

  • A página está carregando, e a linha de execução principal está ocupada fazendo o trabalho inicial de configurar o DOM, organizar e estilizar a 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 futuras, como visto no último exemplo.

A página de demonstração tem um recurso secreto: 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 do bounce e veja qual INP você consegue 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 quicar, 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 de como ele pode ser identificado nos dados de atribuição do INP.

Confira um exemplo de atribuição ao focar apenas a caixa de pesquisa durante o movimento 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. 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 outra coisa na página que atrasou o início do processamento da interação. Mas como saber 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
}]

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

Assim, podemos ver como a função que atrasou o processamento da interação foi acionada (por um listener animationiteration), 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 é renderizada

O atraso na apresentação mede o tempo entre a conclusão da execução dos listeners de eventos e a capacidade do navegador de renderizar um novo frame na tela, mostrando o feedback visível para o usuário.

Atualize a página para redefinir o valor de INP e abra o menu de três traços. 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 na apresentação que representa a maior parte da interação lenta. Isso significa que o que estiver bloqueando a linha de execução principal vai ocorrer depois que os listeners de eventos forem 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 na apresentação é causado por um callback requestAnimationFrame.

9. Conclusão

Agregação de 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 dificulta ainda mais.

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

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 beacon, 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 ele cumpre.

Da mesma forma, as entradas scripts podem ter hashes baseados em arquivos nos caminhos sourceURL, o que dificulta a combinação. No entanto, é possível 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 usar 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 de depuração poderosa. Ele oferece dados granulares sobre o que aconteceu especificamente durante uma INP. Em muitos casos, ele pode indicar o local exato em um script onde você deve começar seus esforços de 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 seguinte snippet no console do DevTools para ver 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