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

Freelance UK web developer, writer, and speaker. Has been around a long time and rants about standards and performance.