Paralaxe com performance

Robert Flack
Robert Flack

Goste ou não, o efeito parallax veio para ficar. Quando usado com cuidado, ele pode adicionar profundidade e sutileza a um app da Web. O problema, no entanto, é que implementar a paralaxe de maneira eficiente pode ser desafiador. Neste artigo, vamos discutir uma solução que tem bom desempenho e, igualmente importante, funciona 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.
  • Use transformações 3D do CSS para criar um efeito de paralaxe mais preciso.
  • Para o 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 do GitHub com exemplos de elementos da interface e salve o JS auxiliar paralaxe. Confira uma demonstração ao vivo do scroller de paralaxe no repositório do GitHub.

Problemas de paralaxe

Para começar, vamos analisar duas maneiras comuns de criar um efeito de paralaxe e, em particular, por que elas não são adequadas para nossos propósitos.

Incorreto: usar eventos de rolagem

O principal requisito da paralaxe é que ela seja acoplada à rolagem. Para cada mudança na posição de rolagem da página, a posição do elemento de paralaxe precisa ser atualizada. Embora isso 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 enviados como "melhor esforço" e não há garantia de que serão enviados em todos os frames da animação de rolagem.

Essa informação importante explica 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 acompanhar a posição de rolagem da página. Nas versões mais antigas do Mobile Safari, os eventos de rolagem eram enviados ao final da rolagem, o que tornava impossível criar um efeito de rolagem baseado em JavaScript. As versões mais recentes fornecem eventos de rolagem durante a animação, mas, assim como o Chrome, com base no "melhor esforço". Se a linha de execução principal estiver ocupada com qualquer outro trabalho, os eventos de rolagem não serão enviados imediatamente, o que significa que o efeito de paralaxe será perdido.

Incorreto: atualização de background-position

Outra situação que gostaríamos de evitar é pintar em todos os frames. Muitas soluções tentam mudar background-position para fornecer a aparência de paralaxe, o que faz com que o navegador pinte novamente as partes afetadas da página ao rolar, e isso pode ser caro o suficiente para prejudicar significativamente a animação.

Para cumprir a promessa do movimento de paralaxe, precisamos de algo que possa ser aplicado como uma propriedade acelerada (o que hoje 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 trabalhos importantes na área de uso do CSS 3D para alcançar o movimento de paralaxe, e a técnica que eles usam é basicamente esta:

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

O CSS para essa abordagem é assim:

.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);
}

Ele pressupõe um snippet de HTML como este:

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

Como ajustar a escala para a perspectiva

Empurrar o elemento filho para trás faz com que ele fique menor proporcionalmente ao valor da perspectiva. É possível calcular quanto será necessário aumentar o tamanho com esta equação: (perspectiva - distância) / perspectiva. Como provavelmente queremos que o elemento de paralaxe tenha paralaxe, mas apareça no tamanho que criamos, ele precisa ser aumentado dessa forma, em vez de ser deixado como está.

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

Para qualquer conteúdo que não tenha um valor translateZ aplicado, você pode substituir um valor de zero. Isso significa que a escala é (perspective - 0) / perspective, o que resulta em um valor de 1, o que significa que ela não foi escalonada para cima nem para baixo. Muito útil, na verdade.

Como essa abordagem funciona

É importante entender por que isso funciona, já que vamos usar esse conhecimento em breve. O rolagem é uma transformação, e por isso pode ser acelerada. Ela envolve principalmente a mudança de camadas com a GPU. Em uma rolagem comum, ou seja, sem nenhuma noção de perspectiva, a rolagem acontece de maneira individual ao comparar o elemento de rolagem e os filhos dele. Se você rolar um elemento para baixo até 300px, os filhos dele serão transformados pelo mesmo valor: 300px.

No entanto, aplicar um valor de perspectiva ao elemento de rolagem interfere com esse processo. Isso muda as matrizes que sustentam a transformação de rolagem. Agora, um rolagem de 300 px pode mover as crianças por apenas 150 px, dependendo dos valores de perspective e translateZ escolhidos. Se um elemento tiver um valor translateZ de 0, ele será rolado em 1:1 (como antes), mas um filho empurrado em Z para longe da origem da perspectiva será rolado em uma taxa diferente. Resultado líquido: movimento de paralaxe. E, muito importante, isso é processado automaticamente como parte da maquinaria de rolagem interna do navegador, o que significa que não é preciso detectar eventos scroll ou mudar background-position.

Uma mosca na sopa: Safari para dispositivos móveis

Há ressalvas para cada efeito, e uma importante para transformações é a preservação de efeitos 3D para elementos filhos. Se houver elementos na hierarquia entre o elemento com uma perspectiva e os filhos com paralaxe, a perspectiva 3D será "achatada", ou seja, o efeito será perdido.

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

No HTML acima, o .parallax-container é novo e vai achatar o valor perspective, fazendo com que o efeito de paralaxe seja perdido. A solução, na maioria dos casos, é bastante simples: adicione transform-style: preserve-3d ao elemento para que ele propague todos os efeitos 3D (como nosso valor de perspectiva) que foram aplicados mais acima na árvore.

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

No caso do Safari para dispositivos móveis, as coisas são um pouco mais complicadas. Aplicar overflow-y: scroll ao elemento do contêiner funciona tecnicamente, mas com o custo de poder deslizar o elemento de rolagem. A solução é adicionar -webkit-overflow-scrolling: touch, mas isso também vai nivelar o perspective e não vamos ter nenhum efeito de paralaxe.

Do ponto de vista de melhoria progressiva, isso provavelmente não é um problema muito grande. Se não for possível usar a paralaxe em todas as situações, o app ainda vai funcionar, mas seria bom encontrar uma solução alternativa.

position: sticky ao resgate!

Na verdade, há uma ajuda na forma de position: sticky, que existe para permitir que os elementos "fiquem presos" à parte de cima da viewport ou a um determinado elemento pai durante a rolagem. A especificação, como a maioria delas, é bastante pesada, mas contém uma pequena joia útil:

Isso pode não parecer muito importante à primeira vista, mas um ponto importante dessa frase é quando ela se refere exatamente a como a adesão de um elemento é calculada exatamente: "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 foi calculado em 300 px, há uma nova oportunidade de usar perspectivas (ou qualquer outra transformação) para manipular esse valor de deslocamento de 300 px antes de ser aplicado a elementos fixos.

Ao aplicar position: -webkit-sticky ao elemento de paralaxe, podemos "reverter" o efeito de achatamento 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, neste caso, é .container. Em seguida, de forma semelhante a antes, a .parallax-container aplica um valor perspective, que muda o deslocamento de rolagem calculado e cria um efeito 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 de paralaxe no Safari para dispositivos móveis, o que é uma ótima notícia para todos.

Advertências sobre posicionamento fixo

No entanto, uma diferença aqui: o position: sticky altera a mecânica de paralaxe. A posição fixa tenta fixar o elemento no contêiner de rolagem, enquanto uma versão não fixa não faz isso. Isso significa que a paralaxe com fixação acaba sendo o inverso da outra sem:

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

Se isso parecer um pouco abstrato, confira esta demonstração de Robert Flack, que demonstra como os elementos se comportam de maneira diferente com e sem o posicionamento fixo. Para notar a diferença, você precisa do Chrome Canary (versão 56 no momento da escrita) ou do Safari.

Captura de tela da perspectiva do paralaxe

Uma demonstração de Robert Flack (em inglês) mostrando como position: sticky afeta a rolagem de paralaxe.

Vários bugs e soluções alternativas

No entanto, como em qualquer caso, ainda há protuberâncias e protuberâncias que precisam ser suavizadas:

  • O suporte a "sticky" é inconsistente. O suporte ainda está sendo implementado no Chrome, o Edge não tem suporte e o Firefox tem bugs de pintura quando a aderência é combinada com transformações de perspectiva. Nesses casos, vale a pena adicionar um pequeno código para adicionar position: sticky (a versão com o prefixo -webkit-) apenas quando necessário, que é somente para o Mobile Safari.
  • O efeito não "simplesmente funciona" no Edge. O Edge tenta processar a rolagem no nível do SO, o que geralmente é uma coisa boa, mas, nesse caso, ele impede a detecção das 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 leve em conta 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, você poderá ver barras de rolagem e similares, mesmo que o elemento esteja em 1x depois que o perspective tiver sido aplicado. É possível contornar esse problema redimensionando elementos do canto inferior direito (com transform-origin: bottom right). Isso funciona porque faz com que elementos grandes cresçam na "região negativa" (normalmente no canto superior esquerdo) da área rolável. As regiões roláveis nunca permitem que você veja ou role o conteúdo na região negativa.

Conclusão

A paralaxe é um efeito divertido quando usado com cuidado. Como você pode ver, é possível implementá-lo de uma maneira que tenha bom desempenho, seja acoplado ao rolagem e funcione em vários navegadores. Como ele requer um pouco de manobra matemática e uma pequena quantidade de boilerplate para conseguir o efeito desejado, criamos uma pequena biblioteca auxiliar e uma amostra, que você pode encontrar no nosso repositório de amostras de elementos da interface do GitHub.

Teste e nos diga o que achou.