Paralaxina'

Introdução

Os sites com paralaxe estão na moda ultimamente. Confira estes exemplos:

Se você não conhece esse tipo de site, eles são aqueles em que a estrutura visual da página muda conforme você rola a tela. Normalmente, os elementos na página são dimensionados, girados ou movidos proporcionalmente à posição de rolagem na página.

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

Não importa se você gosta ou não de sites com paralaxe, mas o que você pode dizer com segurança é que eles são um buraco negro de performance. Isso ocorre porque os navegadores tendem a ser otimizados para o caso em que novos conteúdos aparecem na parte de cima ou de baixo da tela quando você rola (dependendo da direção da rolagem). Em termos gerais, os navegadores funcionam melhor quando há poucas mudanças visuais durante a rolagem. Em um site de paralaxe, isso raramente acontece, porque muitos elementos visuais grandes mudam por toda a página, fazendo com que o navegador pinte novamente toda a página.

É razoável generalizar um site de paralaxe assim:

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

Já abordamos a performance 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 se você ainda não fez isso.

A questão é: se você está criando um site de rolagem parallax, você está preso a repintagens caras ou há outras abordagens que podem ser usadas para maximizar o desempenho? Vamos conferir nossas opções.

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

Essa parece ser a abordagem padrão que a maioria das pessoas adota. Há muitos elementos na página, e sempre que um evento de rolagem é acionado, muitas atualizações visuais são feitas para transformá-los.

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

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

É importante lembrar que, para atingir 60 fps (correspondendo à taxa de atualização típica do monitor de 60 Hz), temos pouco mais de 16 ms para fazer tudo. Nesta primeira versão, estamos realizando as atualizações visuais sempre que recebemos um evento de rolagem. No entanto, como discutimos em artigos anteriores sobre animações mais simples e eficientes com requestAnimationFrame e performance de rolagem, isso não coincide com a programação de atualização do navegador. Portanto, perdemos frames ou fazemos muito trabalho em cada um deles. Isso pode resultar em uma sensação instável e não natural no seu site, o que leva a usuários desapontados 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, talvez note uma pequena melhoria, mas não muito. O motivo é que a operação de layout que acionamos com a rolagem não é tão cara, mas em outros casos de uso, pode ser. Agora, estamos realizando apenas uma operação de layout em cada frame.

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

Agora podemos processar um ou cem eventos de rolagem por frame, mas armazenamos apenas o valor mais recente para uso sempre que o callback requestAnimationFrame é executado e realiza nossas atualizações visuais. O ponto é que você passou de tentar forçar atualizações visuais toda vez que recebe um evento de rolagem para solicitar que o navegador forneça uma janela adequada para isso. Você é um amor.

O principal problema dessa abordagem, requestAnimationFrame ou não, é que temos essencialmente uma camada para toda a página e, ao mover esses elementos visuais, precisamos de recolorações 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, excedemos o orçamento de 16 ms do frame e as coisas ficam instáveis.

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

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

Isso significa que, com essa opção, as coisas são diferentes: podemos ter uma camada para qualquer elemento a que aplicamos uma transformação 3D. Se tudo o que fizermos a partir deste ponto for mais transformações nos elementos, não será necessário repintar a camada, e a GPU poderá lidar com a movimentação dos elementos e a composição da página final.

Muitas vezes, as pessoas usam o hack -webkit-transform: translateZ(0); e notam melhorias mágicas no desempenho. Embora isso funcione hoje, há problemas:

  1. Não é compatível com vários navegadores.
  2. Ele força a mão do navegador criando uma nova camada para cada elemento transformado. Muitas camadas podem causar outros gargalos de desempenho. Use com moderação.
  3. Ele foi desativado para algumas portas do WebKit (quarto ponto da parte de baixo).

Se você seguir a rota de tradução 3D, tenha cuidado, é uma solução temporária para o problema. Idealmente, as características de renderização das transformações 2D seriam semelhantes às do 3D. Os navegadores estão progredindo a uma taxa fenomenal, então esperamos que isso aconteça antes disso.

Por fim, evite fazer pinturas sempre que possível e simplesmente mova os elementos existentes pela página. Por exemplo, é uma abordagem típica em sites com paralaxe usar divs de altura fixa e mudar a posição do plano de fundo para gerar o efeito. Isso significa que o elemento precisa ser pintado novamente em cada passagem, o que pode afetar a performance. Em vez disso, se possível, crie o elemento (envolva-o dentro de um div com overflow: hidden, se necessário) e simplesmente traduza-o.

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

A última opção que vamos considerar é usar uma tela de posição fixa na parte de trás da página em que vamos desenhar as imagens transformadas. À primeira vista, essa pode não parecer a solução com melhor desempenho, mas essa abordagem tem alguns benefícios:

  • Não precisamos mais de tanto trabalho do compositor porque só temos um elemento, a tela.
  • Estamos lidando com um único bitmap acelerado por hardware.
  • A API Canvas2D é uma ótima opção 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 nos dá uma nova camada, mas é apenas uma. Já na opção 2, recebemos uma nova camada para cada elemento com uma transformação 3D aplicada, então temos uma carga de trabalho maior ao compor todas essas camadas. Essa também é a solução mais compatível atualmente, considerando as diferentes implementações de transformações em vários navegadores.


/**
 * 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 quando você lida com imagens grandes (ou outros elementos que podem ser facilmente gravados em uma tela) e, certamente, lidar com grandes blocos de texto seria mais desafiador, mas, dependendo do seu site, pode ser a solução mais adequada. Se você precisar lidar com texto na tela, use o método da API fillText, mas isso vai afetar a acessibilidade (você acabou de rasterizar o texto em um bitmap!) e agora terá que lidar com quebra de linha e várias outras questões. Se possível, evite esse método. Você provavelmente vai ter um melhor resultado usando a abordagem de transformações acima.

Como estamos levando isso o mais longe possível, não há motivo para presumir que o trabalho de paralaxe precisa ser feito dentro de um elemento de tela. Se o navegador for compatível, poderemos usar a WebGL. O ponto principal é que o WebGL tem a rota mais direta de todas as APIs para a placa de vídeo e, como tal, é o candidato mais provável para alcançar 60 fps, especialmente se os efeitos do site forem complexos.

Sua reação imediata pode ser que o WebGL é exagerado ou que não é onipresente em termos de suporte, mas se você usar algo como o Three.js, poderá sempre usar um elemento de tela e seu código será abstrato de maneira consistente e amigável. Tudo o que precisamos fazer é usar o Modernizr para verificar o suporte adequado à API:

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

Por fim, se você não gosta de adicionar elementos extras à página, use uma tela como elemento de plano de fundo em navegadores baseados no Firefox e no WebKit. Isso não é onipresente, obviamente, então, como de costume, você precisa ter cuidado.

A escolha é sua

A principal razão pela qual os desenvolvedores usam elementos posicionados de forma absoluta em vez de qualquer outra opção pode ser simplesmente a ubiquidade do suporte. Isso é, em certa medida, ilusório, já que os navegadores mais antigos que estão sendo segmentados provavelmente oferecem uma experiência de renderização extremamente ruim. Mesmo nos navegadores modernos de hoje, o uso de elementos posicionados de forma absoluta não resulta necessariamente em um bom desempenho.

As transformações, certamente as 3D, permitem trabalhar diretamente com elementos DOM e alcançar uma taxa de frames sólida. O segredo aqui é evitar pintar sempre que possível e simplesmente tentar mover os elementos. Não se esqueça de que a maneira como os navegadores WebKit criam camadas não necessariamente se correlaciona com outros mecanismos de navegador. Portanto, teste antes de se comprometer com essa solução.

Se você está mirando apenas o nível mais alto de navegadores e consegue renderizar o site usando telas, essa pode ser a melhor opção. Se você usar o Three.js, poderá trocar de renderizador com muita facilidade, dependendo do suporte necessário.

Conclusão

Analisamos algumas abordagens para lidar com sites de paralaxe, desde elementos posicionados de forma absoluta até o uso de uma tela de posição fixa. A implementação que você usa depende, é claro, do que você está tentando alcançar e do design específico com que está trabalhando, mas é sempre bom saber que você tem opções.

E, como sempre, não importa qual abordagem você tentar, não faça suposições, teste.