Corrigir instabilidade de layout

Um tutorial sobre como usar o WebPageTest para identificar e corrigir problemas de instabilidade do layout.

Em uma postagem anterior, escrevi sobre como medir a Cumulative Layout Shift (CLS) no WebPageTest. A CLS é uma agregação de todas as mudanças de layout. Por isso, nesta postagem, achei interessante aprofundar e inspecionar cada mudança de layout individual em uma página para tentar entender o que poderia estar causando a instabilidade e corrigir os problemas.

Usando a API Layout Instability, podemos ter uma lista de todos os eventos de mudança de layout em uma página:

new Promise(resolve => {
  new PerformanceObserver(list => {
    resolve(list.getEntries().filter(entry => !entry.hadRecentInput));
  }).observe({type: "layout-shift", buffered: true});
}).then(console.log);

Isso produz uma matriz de trocas de layout que não são precedidas por eventos de entrada:

[
  {
    "name": "",
    "entryType": "layout-shift",
    "startTime": 210.78500000294298,
    "duration": 0,
    "value": 0.0001045969445437389,
    "hadRecentInput": false,
    "lastInputTime": 0
  }
]

Neste exemplo, houve uma única mudança muito pequena de 0,01% em 210 ms.

Saber o tempo e a gravidade da mudança é útil para ajudar a restringir o que pode ter causado a mudança. Vamos voltar ao WebPageTest para um ambiente de laboratório e fazer mais testes.

Como medir mudanças de layout no WebPageTest

Assim como a medição da CLS no WebPageTest, a medição de mudanças de layout individuais vai exigir uma métrica personalizada. Felizmente, o processo é mais fácil agora que o Chrome 77 está estável. A API Layout Instability é ativada por padrão. Assim, você pode executar esse snippet de JS em qualquer site no Chrome 77 e receber resultados imediatamente. No WebPageTest, você pode usar o navegador Chrome padrão sem se preocupar com sinalizações de linha de comando ou com o uso do Canary.

Vamos modificar esse script para produzir uma métrica personalizada para o WebPageTest:

[LayoutShifts]
return new Promise(resolve => {
  new PerformanceObserver(list => {
    resolve(JSON.stringify(list.getEntries().filter(entry => !entry.hadRecentInput)));
  }).observe({type: "layout-shift", buffered: true});
});

A promessa neste script é resolvida como uma representação JSON da matriz, e não a própria matriz. Isso ocorre porque as métricas personalizadas só podem produzir tipos de dados primitivos, como strings ou números.

O site que vou usar para o teste é ismyhostfastyet.com, que criei para comparar o desempenho de carregamento real de hosts da Web.

Como identificar causas de instabilidade de layout

Nos resultados, podemos ver que a métrica personalizada do LayoutShift tem este valor:

[
  {
    "name": "",
    "entryType": "layout-shift",
    "startTime": 3087.2349999990547,
    "duration": 0,
    "value": 0.3422101449275362,
    "hadRecentInput": false,
    "lastInputTime": 0
  }
]

Para resumir, há uma única mudança de layout de 34,2% acontecendo aos 3.087 ms. Para ajudar a identificar o problema, vamos usar a visualização de tira de filme do WebPageTest.

Duas células na tira de filme, mostrando capturas de tela antes e depois da mudança de layout.
Duas células na miniatura, mostrando capturas de tela antes e depois da mudança de layout.

Rolar até a marca de cerca de 3 segundos na tira de filme mostra exatamente qual é a causa da mudança de layout de 34%: a tabela colorida. O site busca um arquivo JSON de forma assíncrona e o renderiza em uma tabela. A tabela está inicialmente vazia, por isso esperar para preenchê-la quando os resultados forem carregados está causando a mudança.

Cabeçalho de fonte da Web aparecendo do nada.
Cabeçalho de fonte da Web aparecendo do nada.

Mas isso não é tudo. Quando a página fica visualmente completa em cerca de 4,3 segundos, podemos ver que o <h1> da página "Meu host já está rápido?" aparece do nada. Isso acontece porque o site usa uma fonte da Web e não tomou nenhuma medida para otimizar a renderização. O layout não parece mudar quando isso acontece, mas a experiência do usuário ainda é ruim porque ele precisa esperar muito para ler o título.

Como corrigir a instabilidade do layout

Agora que sabemos que a tabela gerada de forma assíncrona está causando a mudança de um terço da viewport, é hora de corrigir o problema. Não sabemos o conteúdo da tabela até que os resultados JSON sejam carregados, mas ainda podemos preencher a tabela com algum tipo de dados de marcador de posição para que o layout seja relativamente estável quando o DOM for renderizado.

Confira o código para gerar dados de marcador de posição:

function getRandomFiller(maxLength) {
  var filler = '█';
  var len = Math.ceil(Math.random() * maxLength);
  return new Array(len).fill(filler).join('');
}

function getRandomDistribution() {
  var fast = Math.random();
  var avg = (1 - fast) * Math.random();
  var slow = 1 - (fast + avg);
  return [fast, avg, slow];
}

// Temporary placeholder data.
window.data = [];
for (var i = 0; i < 36; i++) {
  var [fast, avg, slow] = getRandomDistribution();
  window.data.push({
    platform: getRandomFiller(10),
    client: getRandomFiller(5),
    n: getRandomFiller(1),
    fast,
    avg,
    slow
  });
}
updateResultsTable(sortResults(window.data, 'fast'));

Os dados do marcador são gerados aleatoriamente antes de serem classificados. Ele inclui o caractere "█" repetido um número aleatório de vezes para criar marcadores visuais para o texto e uma distribuição gerada aleatoriamente dos três valores principais. Também adicionei alguns estilos para desaturar todas as cores da tabela e deixar claro que os dados ainda não foram totalmente carregados.

A aparência dos marcadores de posição usados não importa para a estabilidade do layout. O objetivo dos marcadores de posição é garantir aos usuários que o conteúdo está chegando e que a página não está corrompida.

Confira como os marcadores de posição ficam enquanto os dados JSON estão sendo carregados:

A tabela de dados é renderizada com dados de marcador de posição.
A tabela de dados é renderizada com dados de marcador de posição.

Resolver o problema da fonte da Web é muito mais simples. Como o site usa as fontes do Google, basta transmitir a propriedade display=swap na solicitação do CSS. Isso é tudo. A API Fonts vai adicionar o estilo font-display: swap na declaração de fonte, permitindo que o navegador renderize o texto em uma fonte de fallback imediatamente. Confira a marcação correspondente com a correção incluída:

<link href="https://fonts.googleapis.com/css?family=Chivo:900&display=swap" rel="stylesheet">

Como verificar as otimizações

Depois de executar a página novamente pelo WebPageTest, podemos gerar uma comparação antes e depois para visualizar a diferença e medir o novo grau de instabilidade do layout:

Filme de WebPageTest mostrando os dois sites sendo carregados lado a lado com e sem otimizações de layout.
Filme do WebPageTest mostrando os dois sites sendo carregados lado a lado com e sem otimizações de layout.
[
  {
    "name": "",
    "entryType": "layout-shift",
    "startTime": 3070.9349999997357,
    "duration": 0,
    "value": 0.000050272187989256116,
    "hadRecentInput": false,
    "lastInputTime": 0
  }
]

De acordo com a métrica personalizada, ainda há uma mudança de layout ocorrendo em 3071 ms (aproximadamente o mesmo tempo anterior), mas a gravidade da mudança é muito menor: 0,005%. Posso viver com isso.

Também é claro na tira de filme que a fonte <h1> volta imediatamente para uma fonte do sistema, permitindo que os usuários a leiam mais cedo.

Conclusão

Sites complexos provavelmente vão ter muitas mais mudanças de layout do que neste exemplo, mas o processo de correção é o mesmo: adicione métricas de instabilidade de layout ao WebPageTest, faça uma referência cruzada dos resultados com a filmagem de carregamento visual para identificar os culpados e implemente uma correção usando marcadores de posição para reservar o espaço da tela.

(One more thing) Como medir a instabilidade do layout experimentada por usuários reais

É bom poder executar o WebPageTest em uma página antes e depois de uma otimização e notar uma melhoria na métrica, mas o que realmente importa é que a experiência do usuário esteja melhorando. Não é por isso que estamos tentando melhorar o site?

Portanto, seria ótimo se começamos a medir as experiências de instabilidade de layout de usuários reais junto com nossas métricas tradicionais de desempenho da Web. Esse é um elemento crucial do ciclo de feedback de otimização, porque ter dados do campo nos informa onde estão os problemas e se nossas correções fizeram diferença.

Além de coletar seus próprios dados de instabilidade de layout, confira o Relatório de UX do Chrome, que inclui dados de deslocamento cumulativo de layout de experiências reais de usuários em milhões de sites. Com ele, você pode descobrir como você (ou seus concorrentes) está se saindo ou analisar o estado de instabilidade do layout na Web.