Paralaxina'

Introdução

Os sites de paralaxe estão em alta recentemente. Confira:

Se você não estiver familiarizado com elas, são os sites em que a estrutura visual da página muda conforme você rola a tela. Normalmente, os elementos da página dimensionam, giram ou se movem proporcionalmente à posição de rolagem.

Uma página de demonstração de paralaxe
Nossa página de demonstração completa com efeito paralaxe

Se você gosta ou não de paralaxe os sites é uma coisa, mas o que você pode dizer com certeza é que eles são um buraco negro de desempenho. O motivo disso é que os navegadores tendem a ser otimizados quando novos conteúdos aparecem na parte de cima ou de baixo da tela quando você rola a tela (dependendo da direção da rolagem). Em termos gerais, os navegadores funcionam melhor quando pouquíssimas mudanças visuais ocorrem durante a rolagem. No caso de um site de paralaxe, isso raramente acontece, porque elementos visuais grandes em toda a página mudam, fazendo com que o navegador repinte toda a página.

É razoável generalizar um site com efeito paralaxe como este:

  • Elementos de plano de fundo que mudam a posição, a rotação e a escala conforme você rola para cima e para baixo.
  • Conteúdo da página, como texto ou imagens menores, que rola da maneira típica de cima para baixo.

Já abordamos o desempenho de rolagem e as maneiras de melhorar a capacidade de resposta do app. Este artigo se baseia nessa base, então vale a pena ler isso, se você ainda não fez isso.

Portanto, se você está criando um site com rolagem paralaxe, está preso a repinturas caras ou há abordagens alternativas que podem ser adotadas para maximizar o desempenho? Vamos conferir as opções.

Opção 1: usar elementos DOM e posições absolutas

Essa parece ser a abordagem padrão adotada pela maioria das pessoas. Há vários elementos na página e, sempre que um evento de rolagem é disparado, várias atualizações visuais são feitas para transformá-los.

Se você iniciar a linha do tempo do DevTools no modo de frame e rolar a tela, vai notar que há operações caras de pintura em tela cheia. Se rolar muito, você poderá ver vários eventos de rolagem dentro de um único frame, cada um dos quais acionará o trabalho de layout.

Chrome DevTools sem eventos de rolagem rebaixados.
DevTools mostrando grandes pinturas e vários layouts acionados por eventos em um único frame.

O importante a se ter em mente é que, para alcançar 60 fps (o mesmo que a taxa de atualização normal do monitor de 60 Hz), temos pouco mais de 16 ms para fazer tudo. Nesta primeira versão, fazemos atualizações visuais sempre que temos um evento de rolagem. No entanto, como discutimos em artigos anteriores sobre animações mais enxutas e malvadas com o requestAnimationFrame e o desempenho de rolagem, isso não coincide com o cronograma de atualização do navegador e, por isso, perdemos frames ou fazemos muito trabalho dentro de cada um. Isso poderia facilmente resultar em uma aparência instável e artificial no site, o que levaria a usuários decepcionados e gatinhos infelizes.

Vamos mover o código de atualização do evento de rolagem para um callback requestAnimationFrame e simplesmente capturar o valor de rolagem no callback do evento de rolagem.

Se você repetir o teste de rolagem, vai notar uma pequena melhoria, mas não muito. Isso porque a operação de layout que acionamos com a rolagem não é tão cara, mas, em outros casos de uso, ela poderia ser. Agora, pelo menos, estamos executando apenas uma operação de layout em cada frame.

Chrome DevTools com eventos de rolagem rebaixados.
DevTools mostrando grandes pinturas e vários layouts acionados por eventos em um único frame.

Agora podemos processar 1 ou 100 eventos de rolagem por frame, mas, principalmente, armazenamos apenas o valor mais recente para uso sempre que o callback requestAnimationFrame é executado e executa nossas atualizações visuais. Agora você passou de tentar forçar atualizações visuais toda vez que recebe um evento de rolagem para solicitar que o navegador ofereça uma janela apropriada para fazer isso. Você não é doce?

O principal problema com essa abordagem, requestAnimationFrame ou não, é que essencialmente temos uma camada para a página inteira e, para mover esses elementos visuais, precisamos de repinturas grandes (e caras). Normalmente, a pintura é uma operação de bloqueio (embora isso esteja mudando), o que significa que o navegador não pode fazer nenhum outro trabalho e, muitas vezes, ultrapassamos o orçamento do frame de 16 ms e as coisas continuam instáveis.

Opção 2: usar elementos DOM e transformações 3D

Em vez de usar posições absolutas, outra abordagem possível é aplicar transformações 3D aos elementos. Nessa situação, os elementos com as transformações 3D aplicadas recebem uma nova camada por elemento. Em navegadores WebKit, isso geralmente também causa a mudança para o compositor de hardware. Na opção 1, em contrapartida, tínhamos uma grande camada da página que precisava ser pintada de novo quando algo mudava, e toda a pintura e composição eram processadas pela CPU.

Isso significa que, com esta opção, as coisas são diferentes: potencialmente temos uma camada para qualquer elemento ao qual aplicamos uma transformação 3D. Se tudo o que fizermos a partir deste ponto for mais transformações nos elementos, não precisaremos repintar a camada, e a GPU poderá lidar com o movimento dos elementos e compor a página final em conjunto.

Muitas vezes, as pessoas só usam a invasão do -webkit-transform: translateZ(0); para notar melhorias mágicas de desempenho, e embora isso funcione hoje, existem alguns problemas:

  1. Não é compatível com navegadores.
  2. Ela força a mão do navegador criando uma nova camada para cada elemento transformado. Muitas camadas podem trazer outros gargalos de desempenho, então use com moderação.
  3. Ele foi desativado para algumas portas do WebKit (quarto marcador da parte inferior).

Se você seguir o caminho de translação 3D, seja cauteloso, esta é uma solução temporária para o seu problema! Em condições ideais, veríamos características de renderização semelhantes nas transformações 2D, como acontece com o 3D. Os navegadores estão progredindo a uma velocidade fenomenal, então esperamos que seja o que veremos antes.

Por fim, evite pinturas sempre que possível e simplesmente mova os elementos existentes pela página. Como exemplo, é uma abordagem típica em sites de paralaxe usar divs de altura fixa e alterar sua posição de segundo plano para fornecer o efeito. Infelizmente, isso significa que o elemento precisa ser repintado em cada passagem, o que pode custar caro em termos de desempenho. Em vez disso, você deve, se possível, criar o elemento (envolvê-lo em um div com overflow: hidden, se necessário) e simplesmente traduzi-lo.

Opção 3: usar uma tela de posição fixa ou WebGL

A opção final que vamos considerar é usar uma tela de posição fixa no fundo da página, na qual vamos desenhar nossas imagens transformadas. À primeira vista, essa não parece ser a solução de melhor desempenho, mas essa abordagem tem alguns benefícios:

  • Não precisamos mais de tanto trabalho de compositor porque só temos um elemento, o canvas.
  • No momento, estamos lidando com um único bitmap acelerado por hardware.
  • A API Canvas2D é ideal para o tipo de transformação que queremos realizar, o que significa que o desenvolvimento e a manutenção são mais gerenciáveis.

Usar um elemento de tela cria uma nova camada, mas tem apenas uma camada. Na Opção 2, recebemos uma nova camada para cada elemento com uma transformação 3D aplicada. Portanto, temos uma carga de trabalho maior para compor todas essas camadas juntas. Essa também é a solução mais compatível no momento, considerando as diferentes implementações de Transforms em navegadores diferentes.


/**
 * Updates and draws in the underlying visual elements to the canvas.
 */
function updateElements () {

  var relativeY = lastScrollY / h;

  // Fill the canvas up
  context.fillStyle = "#1e2124";
  context.fillRect(0, 0, canvas.width, canvas.height);

  // Draw the background
  context.drawImage(bg, 0, pos(0, -3600, relativeY, 0));

  // Draw each of the blobs in turn
  context.drawImage(blob1, 484, pos(254, -4400, relativeY, 0));
  context.drawImage(blob2, 84, pos(954, -5400, relativeY, 0));
  context.drawImage(blob3, 584, pos(1054, -3900, relativeY, 0));
  context.drawImage(blob4, 44, pos(1400, -6900, relativeY, 0));
  context.drawImage(blob5, -40, pos(1730, -5900, relativeY, 0));
  context.drawImage(blob6, 325, pos(2860, -7900, relativeY, 0));
  context.drawImage(blob7, 725, pos(2550, -4900, relativeY, 0));
  context.drawImage(blob8, 570, pos(2300, -3700, relativeY, 0));
  context.drawImage(blob9, 640, pos(3700, -9000, relativeY, 0));

  // Allow another rAF call to be scheduled
  ticking = false;
}

/**
 * Calculates a relative disposition given the page's scroll
 * range normalized from 0 to 1
 * @param {number} base The starting value.
 * @param {number} range The amount of pixels it can move.
 * @param {number} relY The normalized scroll value.
 * @param {number} offset A base normalized value from which to start the scroll behavior.
 * @returns {number} The updated position value.
 */
function pos(base, range, relY, offset) {
  return base + limit(0, 1, relY - offset) * range;
}

/**
 * Clamps a number to a range.
 * @param {number} min The minimum value.
 * @param {number} max The maximum value.
 * @param {number} value The value to limit.
 * @returns {number} The clamped value.
 */
function limit(min, max, value) {
  return Math.max(min, Math.min(max, value));
}

Essa abordagem funciona bem quando você lida com imagens grandes (ou outros elementos que podem ser facilmente escritos em uma tela) e certamente lidar com grandes blocos de texto seria mais desafiador, mas, dependendo do site, pode ser a solução mais apropriada. Se você tiver que lidar com texto no canvas, teria que usar o método de API fillText, mas isso acaba prejudicando a acessibilidade (você acabou de transformar o texto em um bitmap) e agora vai ter que lidar com o ajuste de linha e muitos outros problemas. Se puder evitá-lo, é realmente recomendado que você use a abordagem de transformações acima, e provavelmente receberia mais resultados.

Considerando que estamos levando isso o mais longe possível, não há motivo para presumir que o trabalho de paralaxe deve ser feito dentro de um elemento de tela. Se o navegador fosse compatível, poderíamos usar o WebGL. O importante aqui é que o WebGL tem a rota mais direta de todas as APIs para a placa de vídeo e, por isso, é o mais ideal para atingir 60 fps, especialmente se os efeitos do site forem complexos.

A reação imediata pode ser que o WebGL seja um exagero ou que ele não seja onipresente em termos de suporte, mas se você usar algo como o Three.js, sempre poderá voltar a usar um elemento canvas e seu código será abstraído de maneira consistente e amigável. Basta usar o Modernizr para verificar o suporte à API apropriado:

// check for WebGL support, otherwise switch to canvas
if (Modernizr.webgl) {
  renderer = new THREE.WebGLRenderer();
} else if (Modernizr.canvas) {
  renderer = new THREE.CanvasRenderer();
}

Uma última reflexão sobre esta abordagem, se você não é um grande fã de adicionar elementos extras à página, sempre pode usar uma tela como elemento de plano de fundo em navegadores baseados no Firefox e no WebKit. Obviamente, isso não é onipresente. Portanto, como de costume, você deve tratá-lo com cautela.

A escolha é sua

A onipresença do suporte é a principal razão pela qual os desenvolvedores usam elementos posicionados de forma absoluta em vez de qualquer uma das outras opções. Isso é, até certo ponto, ilusório, uma vez que os navegadores mais antigos que estão sendo alvo provavelmente fornecerão uma experiência de renderização extremamente insatisfatória. Mesmo nos navegadores mais recentes, usar elementos posicionados de forma absoluta não resulta necessariamente em um bom desempenho.

As transformações, certamente o 3D, oferecem a possibilidade de trabalhar diretamente com elementos DOM e alcançar uma taxa de quadros sólida. A chave do sucesso aqui é evitar pintar sempre que possível e simplesmente tentar mover os elementos. Tenha em mente que a forma como os navegadores do WebKit criam camadas não está necessariamente relacionada a outros mecanismos de navegação. Por isso, teste antes de se comprometer com a solução.

Se você quer apenas o nível mais alto de navegadores e consegue renderizar o site usando canvas, essa pode ser a melhor opção para você. Se você usasse o Three.js, certamente seria possível alternar entre renderizadores com muita facilidade, dependendo do suporte necessário.

Conclusão

Avaliamos algumas abordagens para lidar com sites de paralaxe, desde elementos posicionados absolutamente até o uso de uma tela de posição fixa. A implementação escolhida vai depender, obviamente, do que você está tentando alcançar e do design específico com o qual você está trabalhando, mas é sempre bom saber que você tem opções.

E, como sempre, use qualquer abordagem: não adivinhe, teste.