Um tutorial sobre como usar o 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, na sigla em inglês) no WebPageTest. A CLS é uma agregação de todas as mudanças de layout. Por isso, neste post, achei interessante analisar cada mudança de layout individual em uma página para tentar entender o que pode estar causando a instabilidade e corrigir os problemas.
Medir trocas de layout
Com a API Layout Instability, podemos receber 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 mudança muito pequena de 0,01% em 210 ms.
Saber a hora e a gravidade da mudança ajuda a restringir o que pode ter causado isso. 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 de CLS no WebPageTest, a medição de trocas de layout individuais exige uma métrica personalizada. Felizmente, o processo ficou mais fácil agora que o Chrome 77 está estável. A API Layout Instability é ativada por padrão. Portanto, 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 flags de linha de comando ou com o 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 nesse script é resolvida para uma representação JSON da matriz, e não para a matriz em si. Isso acontece 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 LayoutShifts tem este valor:
[
{
"name": "",
"entryType": "layout-shift",
"startTime": 3087.2349999990547,
"duration": 0,
"value": 0.3422101449275362,
"hadRecentInput": false,
"lastInputTime": 0
}
]
Em resumo, há uma única mudança de layout de 34,2% acontecendo em 3.087 ms. Para ajudar a identificar o problema, vamos usar a visualização de tira de filme do WebPageTest.

Ao rolar até a marca de 3 segundos na tira de filme, vemos 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. Portanto, aguardar o preenchimento quando os resultados são carregados causa a mudança.

Mas isso não é tudo. Quando a página está visualmente completa em aproximadamente 4,3 segundos, podemos ver 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 nenhuma medida para otimizar a renderização. O layout não parece mudar quando isso acontece, mas ainda é uma experiência ruim para o usuário ter que esperar tanto tempo para ler o título.
Como corrigir a instabilidade de layout
Agora que sabemos que a tabela gerada de forma assíncrona está causando o deslocamento de um terço da janela de visualização, é hora de corrigir isso. 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 em si fique 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 de marcador de posição 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 dessaturar 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 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 que a página não está quebrada.
Veja como os espaços reservados aparecem enquanto os dados JSON são carregados:

Resolver o problema da fonte da Web é muito mais simples. Como o site usa o Google Fonts, basta transmitir a propriedade display=swap
na solicitação CSS. Isso é tudo. A API Fonts adiciona o estilo font-display: swap
na declaração de fonte, permitindo que o navegador renderize o texto em uma fonte substituta 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 no WebPageTest, podemos gerar uma comparação de antes e depois para visualizar a diferença e medir o novo grau de instabilidade 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 3.071 ms (aproximadamente o mesmo tempo de antes), mas a gravidade da mudança é muito menor: 0,005%. Posso viver com isso.
Também fica claro na tira de filme que a fonte <h1>
está imediatamente voltando 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 é o mesmo: adicione métricas de instabilidade de layout ao WebPageTest, faça uma referência cruzada dos resultados com a tira de filme de carregamento visual para identificar os culpados e implemente uma correção usando marcadores de posição para reservar o espaço da tela.
(Mais uma coisa) Medir a instabilidade de 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 em uma 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?
Seria ótimo se começássemos a 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 de otimização, porque ter dados do campo nos informa 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 de mudança de layout cumulativa de experiências de usuários reais em milhões de sites. Com ela, você descobre como você (ou seus concorrentes) estão se saindo ou pode usá-la para analisar o estado da instabilidade de layout na Web.