No último dia 2 de fevereiro de 2017, Ann Taylor escreveu o artigo 12 Things We Need to See From WordPress Page Builders in 2017 + Who Already Gets It Right no CodeInWP. Temos aprimorado continuamente o Page Builder Sandwich há mais de um ano: fazemos nossas pesquisas, ouvimos nossos clientes, então pensamos que o Page Builder Sandwich entraria no artigo “Who Got it Right” da CodeInWP. Não entramos, porém, por causa do desempenho.
Então perguntamos à Ann sobre isso, e ela respondeu:
…Eu testei um pouco a versão gratuita. Ela é bastante promissora: muitos módulos, interface de usuário bem intuitiva, ótima experiência WYSIWYG, tudo é arrastado com facilidade. No entanto, quando coloquei cerca de 10 módulos na página, ficou difícil continuar trabalhando nela devido à baixa velocidade de edição…
Portanto, de acordo com ela, o Page Builder Sandwich tem problemas de desempenho. E como sempre ouvimos o feedback, fomos conferir.
Testando o Desempenho
Precisávamos de uma forma de verificar o desempenho do PBS; como o PBS é executado principalmente no navegador via Javascript, o profiler Timeline do Chrome era a ferramenta certa para o trabalho. O profiler de linha do tempo pode identificar e medir quais processos estão ocorrendo em uma página da web durante o tempo de execução, o que é perfeito para encontrar gargalos de velocidade.
Pensamos em testar o mais simples dos passos primeiro – passar o mouse por vários elementos e linhas. Simples, né? Estávamos confiantes de que não encontraríamos problemas de desempenho com algo tão simples assim. Infelizmente, estávamos errados.
Assim que clicamos no botão de gravar, passamos o mouse por cerca de 2 segundos na tela e depois clicamos no botão de parar. Aqui está a linha do tempo resultante do profiler:
Vejam só. Está repleto de gargalos de desempenho!
Há muitas coisas acontecendo no gráfico, mas o problema mais óbvio é que há muitas áreas roxas nele (o que não deveria acontecer). Além disso, essas áreas roxas são visivelmente muito largas e todas parecem atingir o pico do gráfico. Você também notará que o gráfico está sempre cheio e que as áreas roxas quase nunca caem para zero. Parece que o navegador está sempre ocupado fazendo algo e quase nunca descansando.
Mas o que todas essas áreas roxas realmente significam?
O profiler teve a gentileza de nos informar que elas são chamadas de reflows. Aqui está a definição do que é um reflow no site Google Developers:
Reflow é o nome do processo do navegador web para recalcular as posições e geometrias dos elementos no documento.. reflow é uma operação que bloqueia o usuário no navegador.
Isso significa que passar o mouse pelo PBS aciona uma tonelada de reflows. O que é pior é que cada reflow pausa o navegador momentaneamente para que ele possa terminar seus cálculos. Mais reflows significam mais pausas no navegador, e a experiência fica ainda mais travada (janky). Em termos mais simples, muitas áreas roxas = ruim.
Então no fim das contas a Ann estava certa: nós realmente tínhamos um enorme gargalo de desempenho, então nossa equipe imediatamente pôs mãos à obra para corrigi-lo sem demora.
Corrigindo Nosso Problema de Reflow / Travamento (Jank)
Antes de mais nada, precisávamos pesquisar para descobrir o que causa esse reflow em primeiro lugar. Descobrimos que, aparentemente, muitas coisas comuns em Javascript podem provocar o navegador a causar um reflow.
Quase qualquer função que você possa usar para obter a largura, altura ou posição de um elemento DOM pode disparar um reflow. Paul Irish escreveu uma lista extensa de todas as propriedades e funções em Javascript que podem fazer isso. A má notícia é que o Page Builder Sandwich usa muitas dessas propriedades e funções, e todas elas causam reflow:
- elem.getBoundingClientRect()
- window.getComputedStyle()
- window.scrollY
- e muito mais…
Correção nº 1: Armazenando em Cache Processos Dispendiosos de Reflow
A solução mais óbvia é NÃO usar essas propriedades e funções. No entanto, o PBS depende muito delas e não podemos abrir mão delas.
A solução que nos restou foi reduzir ao máximo as chamadas para essas funções. A parte difícil aqui foi que usávamos essas propriedades e funções muitas vezes em diferentes partes do PBS e, na maioria das vezes, nós as usávamos no mesmo elemento DOM várias vezes. Para corrigir isso, criamos nosso próprio mecanismo de cache para cada propriedade/função problemática.
Por exemplo, a função getBoundingClientRect nos dá as medidas atuais em pixels de topo, esquerda, largura, altura, direita e base de um elemento DOM. Usamos essa função o tempo todo para calcular o local onde desenhar o contorno dos elementos no PBS. Aqui está a função de cache que criamos para isso:
// Cache: Nossos client rects são mantidos aqui, podemos usar isso para limpá-los mais tarde.
var elemsWithBoundingRects = [];
window.pbsGetBoundingClientRect = function( element ) {
// Verifica se já obtivemos o client rect antes.
if ( ! element._boundingClientRect ) {
// Se não, obtém e armazena para uso futuro.
element._boundingClientRect = element.getBoundingClientRect();
elemsWithBoundingRects.push( element );
}
return element._boundingClientRect;
};
Em vez de chamar getBoundingClientRect , passamos a usar pbsGetBoundingClientRect . Agora, toda vez que o client rect é consultado, armazenamos esse valor para referência futura, limitando assim o número de disparos de reflow.
Também precisávamos de uma forma de limpar o cache quando os client rects armazenados se tornassem inválidos. Os valores do client rect armazenado mudam quando o usuário rola a página ou adiciona/altera/remove elementos na tela. Então criamos uma função de limpeza de cache que podemos chamar sempre que precisarmos de valores atualizados de client rect:
var clearClientRects = function() {
var i;
for ( i = 0; i < elemsWithBoundingRects.length; i++ ) {
if ( elemsWithBoundingRects[ i ] ) {
elemsWithBoundingRects[ i ]._boundingClientRect = null;
}
}
elemsWithBoundingRects = [];
};
Também aplicamos esse mesmo mecanismo de cache para outras chamadas de função dispendiosas que costumamos usar.
Correção nº 2: Agrupando Leituras e Escritas Usando FastDom
Implementar a correção nº 1 aumentou o desempenho do PBS, mas não foi suficiente.
Pesquisando um pouco mais no Google, encontramos um artigo de Wilson Page sobre prevenção de ‘layout thrashing’. Wilson diz:
Layout Thrashing (reflow) ocorre quando o JavaScript escreve e depois lê violentamente no DOM, múltiplas vezes, causando reflows no documento.
Aparentemente, não basta limitar suas chamadas a propriedades e funções específicas que disparam reflow. Quando você realiza múltiplas “leituras” e “escritas” sucessivas no DOM, isso também dispara um reflow no navegador. Aqui está um cenário típico, direto do site do Wilson:
// Leitura var h1 = element1.clientHeight; // Escrita (invalida o layout) element1.style.height = (h1 * 2) + 'px'; // Leitura (dispara reflow) var h2 = element2.clientHeight; // Escrita (invalida o layout) element2.style.height = (h2 * 2) + 'px'; // Leitura (dispara reflow) var h3 = element3.clientHeight; // Escrita (invalida o layout) element3.style.height = (h3 * 2) + 'px'; // O documento sofre reflow no final do frame
O exemplo acima dispara 3 reflows porque as dimensões da página inteira são invalidadas após uma escrita. A correção sugerida para isso é reordenar o código para agrupar todas as leituras e escritas juntas, de modo que tudo dispare apenas 1 reflow no navegador:
// Leitura var h1 = element1.clientHeight; var h2 = element2.clientHeight; var h3 = element3.clientHeight; // Escrita (invalida o layout) element1.style.height = (h1 * 2) + 'px'; element2.style.height = (h2 * 2) + 'px'; element3.style.height = (h3 * 2) + 'px'; // O documento sofre reflow no final do frame
O trecho acima faz parecer fácil corrigir o problema. Mas, na realidade, no PBS, ler e escrever coisas que disparam reflow acontece em muitos lugares diferentes e em muitos momentos diferentes durante o ciclo de vida do PBS. Reorganizar milhares de linhas de código Javascript não é viável.
Felizmente para nós, Wilson também forneceu uma solução em seu artigo. Ele criou um pequeno script utilitário para ajudar a agrupar essas leituras e escritas, chamado FastDom.
Aqui está um trecho de código do Page Builder Sandwich que usa FastDom e também inclui nossa Correção nº 1 acima:
// Agrupa as leituras de outras threads.
fastdom.measure( function() {
// Usa nosso client rect em cache.
var rect = window.pbsGetBoundingClientRect( elem );
// Agrupa as escritas de outras threads.
fastdom.mutate( function() {
elem.style.top = ( rect.top + window.pbsScrollY() ) + 'px';
elem.style.height = rect.height + 'px';
elem.style.left = rect.left + 'px';
elem.style.width = rect.width + 'px';
}.bind( this ) );
}.bind( this ) );
O que o FastDom faz é agrupar todas as chamadas fastdom.measure e todas as chamadas fastdom.mutate e executá-las de forma assíncrona, agrupando efetivamente todas as leituras e escritas para reduzir todos os reflows no navegador.
Correção nº 3: Acessar Objetos de Estilo Tem Alto Custo
Também descobrimos, com a ajuda do jsPerf, que algumas funções de obtenção de estilo eram mais rápidas do que outras.
No PBS, geralmente precisamos obter o valor atual de um estilo específico de um elemento. Por exemplo, normalmente usamos isto para obter a margem superior:
var styles = window.getComputedStyle( elem ); var marginTop = styles.marginTop;
Mas descobrimos que fazer isto é muito mais rápido:
var marginTop = elem.style.marginTop;
No entanto, elem.style.marginTop e getComputedStyle são muito diferentes. O primeiro nos fornece apenas o estilo inline do elemento e não leva em consideração nenhuma folha de estilo; já o segundo leva em consideração tanto os estilos inline quanto as folhas de estilo.
Além disso, mesmo quando o resultado de getComputedStyle era armazenado em cache, ainda era muito lento. No fim das contas, acessar o próprio objeto de estilo é o processo lento.
Felizmente para nós, o PBS anexa estilos inline aos elementos e, como os estilos inline têm precedência (na maioria das vezes) sobre as folhas de estilo, haverá casos em que obter o valor do estilo inline será suficiente. Portanto, se combinarmos os dois métodos acima, podemos chegar a um código de execução muito mais rápida:
var marginTop = elem.style.marginTop;
if ( ! marginTop ) {
var styles = window.getComputedStyle( elem );
marginTop = styles.marginTop;
}
Os Resultados das Melhorias de Código
Nossas 3 correções acima exigiram muita pesquisa, testes e implementação prática (e adiaram o lançamento do nosso recurso de Fontes), mas no final valeu muito a pena.
Voltando ao nosso teste inicial de mover o mouse pela tela por 2 segundos. Aqui está a linha do tempo antiga, feia e travada:
E aqui está como ficou nossa nova linha do tempo após as melhorias de desempenho:
Como você pode ver, há menos elementos roxos no gráfico. E há uma diferença perceptível na quantidade de processos sendo executados pelo navegador. As barras retangulares na parte inferior também estão mais finas, o que significa que os processos estão sendo executados mais rapidamente do que antes.
Se você comparar a quantidade de áreas roxas altas/largas nos gráficos antigo e novo, estamos falando de um aumento impressionante de 200% no desempenho! Também estamos vendo aumentos de desempenho em todas as áreas. Nada mal!
Principais Conclusões e Aprendizados
Para ser sincero, não achávamos que o PBS tivesse problemas de desempenho antes de a Ann mencionar isso. Principalmente porque estávamos usando máquinas focadas em desenvolvimento e os gargalos de desempenho não eram tão aparentes. Olhando para trás, checar o desempenho deveria ter sido uma das principais prioridades. Mas pelo menos agora, a partir da versão 4.3, isso foi resolvido.
Para aqueles que desejam melhorar o desempenho do seu próprio plugin do WordPress, especialmente se você também usa muito Javascript, aqui estão as minhas principais conclusões desta experiência:
- Limite/armazene em cache suas chamadas a propriedades/funções que disparam reflow. Use isto como um guia: What forces layout/reflow. The comprehensive list.
- Agrupe “leituras” e “escritas” em estilos do DOM. Use o FastDom para fazer isso por você.
- Use o profiler Timeline do Chrome e certifique-se de ter o mínimo possível de reflows (áreas roxas).