Paralaxe com performance

Robert flack
Robert Flack

Ame-o ou odeie-o, o efeito paralaxe veio para ficar. Quando usada com cuidado, ela pode adicionar profundidade e sutileza a um app da Web. No entanto, o problema é que a implementação da paralaxe de forma eficiente pode ser desafiadora. Neste artigo, vamos discutir uma solução que tenha um bom desempenho e, igualmente importante, que funcione em vários navegadores.

Ilustração de paralaxe.

Texto longo, leia o resumo

  • Não use eventos de rolagem ou background-position para criar animações de paralaxe.
  • Usar transformações CSS 3D para criar um efeito de paralaxe mais preciso
  • No Safari para dispositivos móveis, use position: sticky para garantir que o efeito de paralaxe seja propagado.

Se você quiser a solução drop-in, acesse o repositório de exemplos de elementos da interface do GitHub no GitHub (em inglês) e faça o JS auxiliar do Parallax. Confira uma demonstração ao vivo do botão de rolagem paralaxe no repositório do GitHub.

Paralaxadores de problema

Para começar, vamos analisar duas maneiras comuns de conseguir um efeito de paralaxe e, em particular, por que elas são inadequadas para nossos objetivos.

Ruim: usar eventos de rolagem

O principal requisito do paralaxe é que ele precisa ser associado à rolagem. Para cada mudança na posição de rolagem da página, a posição do elemento de paralaxe precisa ser atualizada. Embora pareça simples, um mecanismo importante dos navegadores modernos é a capacidade de trabalhar de forma assíncrona. Isso se aplica, no nosso caso específico, a eventos de rolagem. Na maioria dos navegadores, os eventos de rolagem são entregues como "melhor esforço" e não há garantia de que isso aconteça em todos os frames da animação de rolagem.

Essa informação importante nos diz por que precisamos evitar uma solução baseada em JavaScript que mova elementos com base em eventos de rolagem: o JavaScript não garante que o paralaxe vai continuar acompanhando a posição de rolagem da página. Em versões mais antigas do Mobile Safari, os eventos de rolagem eram exibidos no final da rolagem, o que tornava impossível criar um efeito de rolagem baseado em JavaScript. Versões mais recentes oferecem eventos de rolagem durante a animação, mas, assim como o Chrome, de acordo com o "melhor esforço". Se a linha de execução principal estiver ocupada com qualquer outro trabalho, os eventos de rolagem não serão entregues imediatamente, o que significa que o efeito paralaxe será perdido.

Ruim: atualizando background-position

Outra situação que gostaríamos de evitar é pintar em todos os quadros. Muitas soluções tentam mudar o background-position para oferecer a aparência de paralaxe, o que faz com que o navegador mostre novamente as partes afetadas da página na rolagem, e isso pode ser caro o suficiente para causar uma instabilidade significativa na animação.

Se quisermos cumprir a promessa do movimento paralaxe, queremos algo que possa ser aplicado como uma propriedade acelerada (o que atualmente significa manter as transformações e a opacidade) e que não dependa de eventos de rolagem.

CSS em 3D

Scott Kellum e Keith Clark fizeram um trabalho significativo na área de uso do CSS 3D para gerar movimento de paralaxe, e a técnica usada é esta:

  • Configure um elemento contêiner para rolar com overflow-y: scroll (e provavelmente overflow-x: hidden).
  • Aplique um valor perspective a esse mesmo elemento e defina perspective-origin como top left ou 0 0.
  • Aos filhos desse elemento, aplique uma translação em Z e redimensione-os para fornecer movimento de paralaxe sem afetar o tamanho deles na tela.

O CSS para essa abordagem tem a seguinte aparência:

.container {
  width: 100%;
  height: 100%;
  overflow-x: hidden;
  overflow-y: scroll;
  perspective: 1px;
  perspective-origin: 0 0;
}

.parallax-child {
  transform-origin: 0 0;
  transform: translateZ(-2px) scale(3);
}

O que pressupõe um snippet de HTML como este:

<div class="container">
    <div class="parallax-child"></div>
</div>

Ajustando escala para perspectiva

Reverter o elemento filho fará com que ele fique menor proporcional ao valor da perspectiva. É possível calcular quanto ele precisará ser ampliado com esta equação: (perspectiva - distância) / perspectiva. Como provavelmente queremos que o elemento de paralaxe paralaxe, mas apareça no tamanho em que você o criou, ele precisaria ser dimensionado dessa forma, em vez de ser deixado como está.

No caso do código acima, a perspectiva é de 1px e a distância Z da parallax-child é de -2px. Isso significa que o elemento precisará ser dimensionado em 3x, que é o valor conectado ao código: scale(3).

Para qualquer conteúdo que não tenha um valor de translateZ aplicado, é possível substituir um valor de zero. Isso significa que a escala é (perspective - 0) / perspective, com um valor de 1, o que significa que o escalonamento não foi feito para cima ou para baixo. Isso é bastante útil.

Como essa abordagem funciona

É importante deixar claro por que isso funciona, pois vamos usar esse conhecimento em breve. A rolagem é efetivamente uma transformação, e é por isso que ela pode ser acelerada. Ela envolve principalmente a mudança de camadas com a GPU. Em uma rolagem típica, que não tem noção de perspectiva, a rolagem acontece de forma individual ao comparar o elemento de rolagem e os filhos dele. Se você rolar um elemento para baixo em 300px, os filhos dele serão transformados para cima no mesmo valor: 300px.

No entanto, a aplicação de um valor de perspectiva ao elemento de rolagem afeta esse processo. Isso muda as matrizes que sustentam a transformação de rolagem. Agora, uma rolagem de 300 pixels só pode mover os filhos em 150 pixels, dependendo dos valores de perspective e translateZ escolhidos. Se um elemento tiver um valor translateZ de 0, ele será rolado em 1:1 (como costumava ser), mas um filho empurrado Z para fora da origem da perspectiva será rolado a uma taxa diferente. Resultado: movimento de paralaxe. E, o mais importante, isso é processado automaticamente como parte do mecanismo de rolagem interno do navegador, o que significa que não é necessário detectar eventos scroll ou mudar background-position.

Uma mosca na pomba: Safari para dispositivos móveis

Há ressalvas para todos os efeitos, e uma importante para as transformações é a preservação de efeitos 3D em elementos filhos. Se houver elementos na hierarquia entre o elemento com uma perspectiva e os filhos de paralaxe, a perspectiva 3D será "nivelada", o que significa que o efeito será perdido.

<div class="container">
    <div class="parallax-container">
    <div class="parallax-child"></div>
    </div>
</div>

No HTML acima, a .parallax-container é nova e nivela o valor perspective e perdemos o efeito paralaxe. Na maioria dos casos, a solução é bastante direta: você adiciona transform-style: preserve-3d ao elemento, fazendo com que ele propague quaisquer efeitos 3D (como o valor da perspectiva) que tenham sido aplicados mais acima na árvore.

.parallax-container {
  transform-style: preserve-3d;
}

No caso do Mobile Safari, porém, as coisas são um pouco mais complicadas. A aplicação de overflow-y: scroll ao elemento de contêiner funciona tecnicamente, mas à custa da capacidade de lançar o elemento de rolagem. A solução é adicionar -webkit-overflow-scrolling: touch, mas isso também nivela a perspective e não haverá paralaxe.

Do ponto de vista do aprimoramento progressivo, isso não é um problema muito grande. Se não for possível paralaxe em todas as situações, nosso app ainda vai funcionar, mas seria bom descobrir uma solução alternativa.

position: sticky ao resgate!

Na verdade, há alguma ajuda na forma de position: sticky, que existe para permitir que os elementos "se fixem" na parte de cima da janela de visualização ou em um determinado elemento pai durante a rolagem. As especificações, como a maioria delas, são bem grandes, mas contêm uma pequena joia útil dentro dela:

Isso pode não parecer muito à primeira vista, mas um ponto importante nessa frase é quando se refere a como exatamente a aderência de um elemento é calculada: "o deslocamento é calculado com referência ao ancestral mais próximo com uma caixa de rolagem". Em outras palavras, a distância para mover o elemento fixo (para que ele apareça anexado a outro elemento ou à janela de visualização) é calculada antes de qualquer outra transformação ser aplicada, não depois. Isso significa que, assim como no exemplo de rolagem anterior, se o deslocamento for calculado em 300 px, há uma nova oportunidade de usar perspectivas (ou qualquer outra transformação) para manipular esse valor de deslocamento de 300 pixels antes que ele seja aplicado a elementos fixos.

Ao aplicar position: -webkit-sticky ao elemento de paralaxe, podemos "reverter" efetivamente o efeito de nivelamento de -webkit-overflow-scrolling: touch. Isso garante que o elemento de paralaxe faça referência ao ancestral mais próximo com uma caixa de rolagem, que, nesse caso, é .container. Em seguida, da mesma forma que antes, .parallax-container aplica um valor perspective, que muda o deslocamento de rolagem calculado e cria um efeito de paralaxe.

<div class="container">
    <div class="parallax-container">
    <div class="parallax-child"></div>
    </div>
</div>
.container {
  overflow-y: scroll;
  -webkit-overflow-scrolling: touch;
}

.parallax-container {
  perspective: 1px;
}

.parallax-child {
  position: -webkit-sticky;
  top: 0px;
  transform: translate(-2px) scale(3);
}

Isso restaura o efeito paralaxe do Mobile Safari, o que é uma excelente notícia.

Ressalvas de posicionamento fixo

uma diferença aqui, no entanto: position: sticky altera a mecanismo de paralaxe. O posicionamento fixo tenta, bem, prender o elemento ao contêiner de rolagem, enquanto uma versão não fixa não. Isso significa que o paralaxe com fixa acaba sendo o inverso daquele sem:

  • Com position: sticky, quanto mais próximo o elemento for de z=0, menos ele se moverá.
  • Sem position: sticky, quanto mais próximo o elemento for de z=0, mais ele se moverá.

Se tudo isso parece um pouco abstrato, confira esta demonstração de Robert Flack, que mostra como os elementos se comportam de maneira diferente com e sem posicionamento fixo. Para ver a diferença, você precisa do Chrome Canary (que é a versão 56 na época em que este artigo foi escrito) ou do Safari.

Captura de tela da perspectiva de paralaxe

Uma demonstração de Robert Flack mostrando como position: sticky afeta a rolagem paralaxe.

Diversos bugs e soluções alternativas

No entanto, como acontece com tudo, ainda há caroços que precisam ser suavizados:

  • O suporte fixo é inconsistente. O suporte ainda está sendo implementado no Chrome, o Edge não é totalmente compatível, e o Firefox apresenta bugs quando o modo aderente é combinado com transformações de perspectiva. Nesses casos, vale a pena adicionar um pequeno código para adicionar apenas position: sticky (a versão com prefixo -webkit-) quando necessário, que é somente para o Mobile Safari.
  • O efeito não "apenas funciona" no Edge. O Edge tenta processar a rolagem no nível do SO, o que geralmente é bom, mas, nesse caso, impede que ele detecte as mudanças de perspectiva durante a rolagem. Para corrigir isso, adicione um elemento de posição fixa, já que ele parece mudar o Edge para um método de rolagem que não é do SO e garante que ele considere as mudanças de perspectiva.
  • "O conteúdo da página ficou enorme!" Muitos navegadores consideram a escala ao decidir o tamanho do conteúdo da página, mas, infelizmente, o Chrome e o Safari não consideram a perspectiva. Portanto, se houver uma escala de 3x aplicada a um elemento, é possível ver barras de rolagem e similares, mesmo que o elemento esteja em 1x após a aplicação de perspective. É possível contornar esse problema dimensionando elementos do canto inferior direito (com transform-origin: bottom right), o que funciona porque faz com que elementos muito grandes cresçam na "região negativa" (geralmente o canto superior esquerdo) da área de rolagem. As regiões roláveis nunca permitem que você veja ou role para o conteúdo na região negativa.

Conclusão

A paralaxe é um efeito divertido quando usado de maneira cuidadosa. É possível implementá-lo de uma maneira eficiente, compatível com rolagem e entre navegadores. Como ela requer um pouco de confusão matemática e uma pequena quantidade de texto boilerplate para alcançar o efeito desejado, reunimos uma pequena biblioteca auxiliar e uma amostra, que podem ser encontradas no nosso repositório de amostras de elementos da interface no GitHub (link em inglês).

Jogue agora e nos conte como você se saiu.