A API de desempenho mede a capacidade de resposta da sua aplicação web ao vivo em dispositivos reais de usuário e conexões de rede. Ela pode ajudar a identificar gargalos no seu código do lado do cliente e do lado do servidor:

  • user timing: Medição personalizada do desempenho da função JavaScript do lado do cliente
  • paint timing: Métricas de renderização do navegador
  • resource timing: Carregamento do desempenho dos ativos e chamadas Ajax
  • navigation timing: Métricas de carregamento de página, incluindo redirecionamentos, consultas DNS, prontidão DOM, e mais

A API aborda vários problemas associados com a avaliação de desempenho típica:

  1. Os desenvolvedores frequentemente testam aplicativos em PCs high-end conectados a uma rede rápida. O DevTools pode emular dispositivos mais lentos, mas nem sempre destacará problemas do mundo real quando a maioria dos clientes está rodando um celular com dois anos de idade conectado ao WiFi do aeroporto.
  2. Opções de terceiros, como o Google Analytics, são frequentemente bloqueadas, levando a resultados e suposições distorcidas. Você também pode encontrar implicações de privacidade em alguns países.
  3. A API de desempenho pode medir com precisão várias métricas melhor do que métodos tais como Date().


As seções seguintes descrevem como você pode usar a API de desempenho. Alguns conhecimentos de JavaScript e métricas de carregamento de página são recomendados.

Disponibilidade da API de desempenho

A maioria dos navegadores modernos suporta a Performance API – incluindo o IE10 e IE11 (até o IE9 tem suporte limitado). Você pode detectar a presença da API usando:

if ('performance' in window) {
  // use Performance API
}

Não é possível efetuar um Polyfill completo do API, portanto, tenha cuidado com a falta de navegadores. Se 90% dos seus usuários estão navegando com o Internet Explorer 8, você estaria medindo apenas 10% dos clientes com aplicativos mais capazes.

A API pode ser usada em Web Worker, que fornecem uma maneira de executar cálculos complexos em uma linha de fundo sem interromper as operações do navegador.

A maioria dos métodos API podem ser usados no server-side Node.js com o módulo perf_hooks padrão:

// Node.js performance
import { performance } from 'node:perf_hooks';
// or in Common JS: const { performance } = require('node:perf_hooks');

console.log( performance.now() );

A Deno fornece a API de desempenho padrão:

// Deno performance
console.log( performance.now() );

Você precisará executar scripts com a permissão --allow-hrtime para permitir a medição de tempo em alta resolução:

deno run --allow-hrtime index.js

O desempenho do lado do servidor geralmente é mais fácil de avaliar e gerenciar porque depende da carga, CPUs, RAM, discos rígidos e limites dos serviços em nuvem. Atualizações de hardware ou opções de gerenciamento de processos como PM2, clustering e Kubernetes podem ser mais eficazes do que o código de refatoração.

As seções seguintes se concentram no desempenho do lado do cliente por esta razão.

Medição de desempenho personalizado

A API de desempenho pode ser usada para cronometrar a velocidade de execução das funções da sua aplicação. Você pode ter usado ou encontrado funções de cronometragem usando Date():

const timeStart = new Date();
runMyCode();
const timeTaken = new Date() - timeStart;

console.log(`runMyCode() executed in ${ timeTaken }ms`);

O API de desempenho oferece dois benefícios principais:

  1. Melhor precisão: Date() mede até o milissegundo mais próximo, mas o API de desempenho pode medir frações de um milissegundo (dependendo do navegador).
  2. Melhor confiabilidade: O usuário ou sistema operacional pode mudar o tempo do sistema de forma que as métricas baseadas em Date() nem sempre serão precisas. Isto significa que suas funções podem parecer particularmente lentas quando os relógios avançam!

O equivalente em Date() é performance.now() que retorna um carimbo de tempo de alta resolução que é colocado em zero quando o processo responsável pela criação do documento começa (a página foi carregada):

const timeStart = performance.now();
runMyCode();
const timeTaken = performance.now() - timeStart;

console.log(`runMyCode() executed in ${ timeTaken }ms`);

Um não-padrão performance.timeOrigin a propriedade também pode devolver um carimbo de data/hora a partir de 1 de janeiro de 1970, embora isso não esteja disponível no IE e Deno.

performance.now() torna-se impraticável quando se faz mais do que algumas medidas. A API de desempenho fornece um buffer onde você pode gravar o evento para análise posterior, passando um nome de etiqueta para performance.mark():

performance.mark('start:app');
performance.mark('start:init');

init(); // run initialization functions

performance.mark('end:init');
performance.mark('start:funcX');

funcX(); // run another function

performance.mark('end:funcX');
performance.mark('end:app');

Um conjunto de todos os objetos de marcação no buffer de performance pode ser extraído usando:

const mark = performance.getEntriesByType('mark');

Exemplo de resultado:

[

  {
    detail: null
    duration: 0
    entryType: "mark"
    name: "start:app"
    startTime: 1000
  },
  {
    detail: null
    duration: 0
    entryType: "mark"
    name: "start:init"
    startTime: 1001
  },
  {
    detail: null
    duration: 0
    entryType: "mark"
    name: "end:init"
    startTime: 1100
  },
...
]

O performance.measure() calcula o tempo entre duas marcas e também o armazena no buffer de desempenho. Você passa um novo nome de medida, o nome da marca inicial (ou nulo para medir a partir do carregamento da página), e o nome da marca final (ou nulo para medir a hora atual):

performance.measure('init', 'start:init', 'end:init');

Um objeto PerformanceMeasure é anexado ao buffer com a duração de tempo calculada. Para obter este valor, você pode solicitar um array de todas as medidas:

const measure = performance.getEntriesByType('measure');

ou solicitar uma medida pelo seu nome:

performance.getEntriesByName('init');

Exemplo de resultado:

[
  {
    detail: null
    duration: 99
    entryType: "measure"
    name: "init"
    startTime: 1001
  }
]

Usando o buffer de desempenho

Além de marcas e medidas, o buffer de desempenho é usado para registrar automaticamente o navigation timing, o resource timing e o paint timing (que discutiremos mais tarde). Você pode obter um array de todas as entradas no buffer:

performance.getEntries();

Por padrão, a maioria dos navegadores fornece um buffer que armazena até 150 métricas de recursos. Isto deve ser suficiente para a maioria das avaliações, mas você pode aumentar ou diminuir o limite de buffer, se necessário:

// record 500 metrics
performance.setResourceTimingBufferSize(500);

As marcas podem ser apagadas pelo nome ou você pode especificar um valor vazio para apagar todas as marcas:

performance.clearMarks('start:init');

Da mesma forma, as medidas podem ser limpas pelo nome ou por um valor vazio para limpar tudo:

performance.clearMeasures();

Monitoramento de atualizações de buffer de desempenho

A PerformanceObserver pode monitorar mudanças no buffer de desempenho e executar uma função quando eventos específicos ocorrem. A sintaxe será familiar se você tiver usado MutationObserver para responder a atualizações do DOM ou IntersectionObserver para detectar quando os elementos são rolados para o viewport.

Você deve definir uma função de observador com dois parâmetros:

  1. uma série de entradas de observadores que foram detectadas, e
  2. o objeto observador. Se necessário, seu disconnect() pode ser chamado para parar o observador.
function performanceCallback(list, observer) {

  list.getEntries().forEach(entry => {
    console.log(`name    : ${ entry.name }`);
    console.log(`type    : ${ entry.type }`);
    console.log(`start   : ${ entry.startTime }`);
    console.log(`duration: ${ entry.duration }`);
  });

}

A função é passada para um novo objeto PerformanceObserver. Seu observe() é passado um conjunto de tipos de entrada de buffer de desempenho a serem observados:

let observer = new PerformanceObserver( performanceCallback );
observer.observe({ entryTypes: ['mark', 'measure'] });

Neste exemplo, a adição de uma nova marca ou medida executa a função performanceCallback(). Enquanto ela apenas registra mensagens aqui, ela poderia ser usada para acionar um upload de dados ou fazer cálculos adicionais.

Medindo o desempenho Paint

O Paint Timing API está disponível apenas em JavaScript do lado do cliente e grava automaticamente duas métricas que são importantes para o Core Web Vitals:

  1. first-paint: O navegador começou a renderizar a página.
  2. first-contentful-paint: O navegador tornou o primeiro elemento significativo do conteúdo DOM, como um título ou uma imagem.

Estes podem ser extraídos do buffer de desempenho para um array:

const paintTimes = performance.getEntriesByType('paint');

Tenha cuidado ao executar isto antes que a página tenha sido totalmente carregada; os valores não estarão prontos. Ou espere pelo window.load evento ou usar um PerformanceObserver para monitorar paint entryTypes.

Exemplo de resultado:

[
  {
    "name": "first-paint",
    "entryType": "paint",
    "startTime": 812,
    "duration": 0
  },
  {
    "name": "first-contentful-paint",
    "entryType": "paint",
    "startTime": 856,
    "duration": 0
  }
]

Um first-paint lenta é frequentemente causada por CSS ou JavaScript. A lacuna para first-paint pode ser grande se o navegador tiver que baixar uma imagem grande ou renderizar elementos complexos.

Medindo o Resource Performance

Os tempos de rede para recursos como imagens, folhas de estilo e arquivos JavaScript são automaticamente gravados no buffer de desempenho. Embora haja pouco que você possa fazer para resolver problemas de velocidade da rede (além de reduzir o tamanho dos arquivos), isso pode ajudar a destacar problemas com ativos maiores, respostas lentas do Ajax ou scripts de terceiros com mau desempenho.

Um conjunto de métricas PerformanceResourceTiming pode ser extraído do buffer usando:

const resources = performance.getEntriesByType('resource');

Alternativamente, você pode buscar métricas para um ativo passando sua URL completa:

const resource = performance.getEntriesByName('https://test.com/script.js');

Exemplo de resultado:

[
  {
    connectEnd: 195,
    connectStart: 195,
    decodedBodySize: 0,
    domainLookupEnd: 195,
    domainLookupStart: 195,
    duration: 2,
    encodedBodySize: 0,
    entryType: "resource",
    fetchStart: 195,
    initiatorType: "script",
    name: "https://test.com/script.js",
    nextHopProtocol: "h3",
    redirectEnd: 0,
    redirectStart: 0,
    requestStart: 195,
    responseEnd: 197,
    responseStart: 197,
    secureConnectionStart: 195,
    serverTiming: [],
    startTime: 195,
    transferSize: 0,
    workerStart: 195
  }
]

As seguintes propriedades podem ser examinadas:

  • name: URL do recurso
  • entryType: “recurso”
  • initiatorType: Como o recurso foi iniciado, tal como “script” ou “link”
  • serverTiming: Um conjunto de PerformanceServerTiming objetos passados pelo servidor no cabeçalho HTTP Server-Timing (sua aplicação do lado do servidor poderia enviar métricas para o cliente para análise posterior)
  • startTime: Timestamp quando o fetch começou
  • nextHopProtocolo: Protocolo de rede utilizado
  • workerStart: Timestamp antes de iniciar um Trabalhador de Serviço Progressivo de Aplicação Web (0 se a solicitação não for interceptada por um Trabalhador de Serviço)
  • redirectStart: Carimbo de tempo quando um redirecionamento começou
  • redirectEnd: Timestamp após o último byte da última resposta de redirecionamento
  • fetchStart: Timestamp antes do recebimento dos recursos
  • domínioLookupStart: Carimbo da hora antes de uma consulta DNS
  • domainLookupEnd: Carimbo de tempo após a pesquisa DNS
  • connectStart: Timestamp antes de estabelecer uma conexão de servidor
  • connectEnd: Timestamp depois de estabelecer uma conexão de servidor
  • secureConnectionStart: Timestamp antes do aperto de mão SSL
  • requestStart: Timestamp antes que o navegador solicite o recurso
  • responseStart: Timestamp quando o navegador recebe o primeiro byte de dados
  • responseEnd: Timestamp depois de receber o último byte ou fechar a conexão
  • duration: A diferença entre startTime e responseEnd
  • transferSize: O tamanho do recurso em bytes incluindo o cabeçalho e o corpo comprimido
  • encodedBodySize: O corpo do recurso em bytes antes de descomprimir
  • decodedBodySize: O corpo do recurso em bytes após descompressão

Este script de exemplo recupera todos os pedidos Ajax iniciados pelo Fetch API e retorna o tamanho total e a duração da transferência:

const fetchAll = performance.getEntriesByType('resource')
  .filter( r => r.initiatorType === 'fetch')
  .reduce( (sum, current) => {
    return {
      transferSize: sum.transferSize += current.transferSize,
      duration: sum.duration += current.duration
    }
  },
  { transferSize: 0, duration: 0 }
);

Medindo o Navigation Performance

Os tempos de rede para baixar a página anterior e carregar a página atual são automaticamente registrados no buffer de desempenho como um único objeto PerformanceNavigationTiming.

Extraia-o para um array usando:

const pageTime = performance.getEntriesByType('navigation');

…ou passando a URL da página para .getEntriesByName():

const pageTiming = performance.getEntriesByName(window.location);

As métricas são idênticas às dos recursos, mas também incluem valores específicos de página:

  • entryType: Por exemplo: “navegação”
  • tipo: Ou “navigate”, “reload”, “back_forward”, ou “prerender”
  • redirectCount: O número de redirecionamentos
  • unloadEventStart: Timestamp antes do evento de descarregamento do documento anterior
  • unloadEventEnd: Timestamp após o evento de descarregamento do documento anterior
  • domInteractive: Timestamp quando o navegador tiver analisado o HTML e construído o DOM
  • domContentLoadedEventStart: Carimbo da hora antes do evento DOMContentLoadedDomContentLoaded do documento
  • domContentLoadedEventEnd: Timestamp após a conclusão do evento DOMContentLoadedDomContentLoaded do documento
  • domComplete: Timestamp após a construção do DOM e eventos DOMContentLoaded
  • loadEventStart: Timestamp antes que o evento de carga de página tenha disparado
  • loadEventEnd: Timestamp após o evento de carregamento da página e todos os ativos estão disponíveis

As questões típicas incluem:

  • Um longo atraso entre unloadEventEnd e domInteractive. Isto pode indicar uma resposta lenta do servidor.
  • Um longo atraso entre domContentLoadedEventStart e domComplete. Isto poderia indicar que os scripts de inicialização da página são muito lentos.
  • Um longo atraso entre domComplete e loadEventEnd. Isso pode indicar que a página tem muitos ativos ou vários estão demorando muito para carregar.

Gravação e análise de desempenho

A API de desempenho permite que você colete dados de uso no mundo real e os carregue em um servidor para análise posterior. Você poderia usar um serviço de terceiros como o Google Analytics para armazenar os dados, mas há um risco de o script de terceiros ser bloqueado ou introduzir novos problemas de desempenho. Sua própria solução pode ser personalizada de acordo com suas necessidades para garantir que o monitoramento não tenha impacto em outras funcionalidades.

Tenha cuidado com situações nas quais as estatísticas não podem ser determinadas – talvez porque os usuários estão em navegadores antigos, bloqueando o JavaScript, ou atrás de um proxy corporativo. Entender quais dados estão faltando pode ser mais proveitoso do que fazer suposições baseadas em informações incompletas.

Idealmente, seus scripts de análise não afetarão negativamente o desempenho executando cálculos complexos ou fazendo upload de grandes quantidades de dados. Considere utilizar trabalhadores da web e minimizar o uso de chamadas sincronizadas do localStorage. É sempre possível processar os dados brutos em lote mais tarde.

Finalmente, desconfie de dispositivos e conexões anômalas, tais como dispositivos muito rápidos ou muito lentos que afetam adversamente as estatísticas. Por exemplo, se nove usuários carregarem uma página em dois segundos, mas o décimo experimentar um download de 60 segundos, a média de latência sai para quase 8 segundos. Uma métrica mais realista é o valor mediano (2 segundos) ou o percentil 90 (9 em cada 10 usuários experimentam um tempo de carregamento de 2 segundos ou menos).

Resumo

O desempenho da Web continua sendo um fator crítico para os desenvolvedores. Os usuários esperam que os sites e aplicativos sejam responsivos na maioria dos dispositivos. A Otimização de mecanismos de busca também pode ser afetada à medida que sites mais lentos são rebaixados no Google.

Existem muitas ferramentas de monitoramento de desempenho por aí, mas a maioria avalia a velocidade de execução do lado do servidor ou usa um número limitado de clientes capazes para julgar a renderização do navegador. A API de desempenho fornece uma forma de comparar métricas reais do usuário que não seria possível calcular de outra forma.

Craig Buckler

Desenvolvedor web freelancer do Reino Unido, escritor e palestrante. Está na área há muito tempo e discursa sobre padrões e desempenho.