Corrigir instabilidade de layout

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

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

Medir mudanças de layout

Usando a API Layout Instability, podemos conseguir 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 mudanças 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 variação muito pequena de 0,01% a 210 ms.

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

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ê poderá executar esse snippet JS em qualquer site no Chrome 77 e receber resultados imediatamente. No WebPageTest, você pode usar o navegador Chrome padrão e não precisa se preocupar com sinalizações de linha de comando ou com o uso do Canary.

Então, 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 nesse script é resolvida em uma representação JSON da matriz, e não na matriz em si. Isso ocorre porque as métricas personalizadas só podem produzir tipos de dados primitivos, como strings ou números.

Usei o site ismyhostfastyet.com, que foi criado para comparar o desempenho real de carregamento dos hosts da Web.

Como identificar causas de instabilidade de layout

Nos resultados, vemos que a métrica personalizada do LayoutShifts 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% ocorrendo a 3.087 ms. Para ajudar a identificar o culpado, vamos usar a visualização de tira de filme do WebPageTest.

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

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

Cabeçalho de fonte da Web aparecendo do nada.
O cabeçalho da fonte da Web aparece do nada.

Mas isso não é tudo. Quando a página estiver visualmente completa em cerca de 4,3 segundos, poderemos notar que o <h1> da página "Is my host fast yet?" aparece do nada. Isso acontece porque o site usa uma fonte da Web e não tomou medidas para otimizar a renderização. O layout não parece mudar quando isso acontece, mas ainda é uma má experiência do usuário ter que esperar tanto tempo para ler o título.

Corrigir a instabilidade do layout

Agora que sabemos que a tabela gerada de forma assíncrona está fazendo com que um terço da janela de visualização mude, é hora de corrigi-la. Não conhecemos o conteúdo da tabela até que os resultados JSON sejam realmente carregados, mas ainda podemos preenchê-la com algum tipo de dados de marcadores para que o layout fique relativamente estável quando o DOM é renderizado.

Confira o código para gerar dados de marcador:

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 de posição visuais para o texto e uma distribuição gerada aleatoriamente dos três valores principais. Também adicionei alguns estilos para diminuir a saturação de todas as cores da tabela, deixando claro que os dados ainda não estão totalmente carregados.

A aparência dos marcadores de posição que você usa 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 a página não está corrompida.

Veja como os marcadores de posição aparecem enquanto os dados JSON são carregados:

A tabela de dados é renderizada com dados de marcadores.
A tabela de dados é renderizada com dados de marcadores.

Resolver o problema de fontes da Web é muito mais simples. Como o site está usando o Google Fonts, basta transmitir a propriedade display=swap na solicitação CSS. Isso é tudo. A API Fonts vai adicionar o estilo font-display: swap na declaração da fonte, permitindo que o navegador renderize o texto em uma fonte substituta imediatamente. Veja 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 usando o WebPageTest, podemos gerar uma comparação de antes e depois para visualizar a diferença e medir o novo grau de instabilidade do layout:

Tira de filme do WebPageTest mostrando os dois sites sendo carregados lado a lado com e sem otimizações de layout.
Tira de 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 aos 3.071 ms (mais ou menos no mesmo período anterior), mas a gravidade da mudança é muito menor: 0,005%. eu posso conviver com isso.

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

Conclusão

Sites complexos provavelmente vão ter muito mais mudanças de layout do que neste exemplo, mas o processo de correção ainda é o mesmo: adicionar métricas de instabilidade de layout ao WebPageTest, comparar os resultados com a tira de filme de carregamento visual para identificar os culpados e implementar uma correção usando marcadores de posição para reservar o espaço da tela.

(Mais uma coisa) Medir a instabilidade de layout de usuários reais

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

Então, o que seria ótimo se pudéssemos medir as experiências de instabilidade de layout de usuários reais junto com nossas métricas tradicionais de desempenho da Web. Essa é uma parte crucial do ciclo de feedback da otimização, porque os dados do campo nos dizem onde estão os problemas e se nossas correções fizeram uma diferença positiva.

Além de coletar seus próprios dados de instabilidade de layout, confira o Chrome UX Report, que inclui dados da Mudança de layout cumulativa de experiências de usuários reais em milhões de sites. Ele permite que você descubra como está o desempenho de você (ou de seus concorrentes) ou pode usá-lo para explorar o estado de instabilidade de layout na Web.