Paralaxe com performance

Robert Flack
Robert Flack

Não se preocupe, o paralaxe veio para ficar. Quando usada criteriosamente, ela adiciona profundidade e sutileza a um app da Web. No entanto, o problema é que implementar o paralaxe de maneira eficiente pode ser um desafio. Neste artigo, discutiremos uma solução que tem bom desempenho e 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 as transformações CSS 3D para criar um efeito de paralaxe mais preciso.
  • No Mobile Safari, use position: sticky para garantir que o efeito 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 botão de rolagem de paralaxe no repositório do GitHub.

Paralaxantes que apresentam problemas

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: uso de eventos de rolagem

O principal requisito do paralaxe é que ele precisa ser acoplado à 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 funcionar 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 exibidos como "melhor esforço", e não há garantia de que eles serão exibidos 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 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 frames. Muitas soluções tentam mudar background-position para fornecer a aparência de paralaxe, o que faz com que o navegador repinte as partes afetadas da página ao rolar a tela, e isso pode ser caro o suficiente para atrapalhar significativamente a animação.

Para cumprir a promessa do movimento de paralaxe, queremos 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

Tanto Scott Kellum quanto Keith Clark fizeram um trabalho significativo na área de uso do CSS 3D para alcançar o movimento de paralaxe, e a técnica que eles usam é efetivamente esta:

  • Configure um elemento contêiner para rolar a tela 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 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);
}

Ele pressupõe um snippet de HTML como este:

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

Ajustando a escala da perspectiva

Enviar o elemento filho de volta fará com que ele fique menor, proporcionalmente ao valor da perspectiva. É possível calcular o quanto ele precisará ser escalonado com esta equação: (perspectiva - distância) / perspectiva. Como provavelmente desejamos que o elemento de paralaxe seja paralaxe, mas aparece com o tamanho que o criou, ele precisaria ser dimensionado dessa forma, em vez de ficar 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 escalonado verticalmente em 3x, que é o valor conectado ao código: scale(3).

Para qualquer conteúdo sem um valor translateZ aplicado, é possível 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. Isso é muito útil.

Como essa abordagem funciona

É importante esclarecer por que isso funciona, já que esse conhecimento será usado em breve. A rolagem é efetivamente uma transformação e, por isso, pode ser acelerada. Ela envolve principalmente a troca 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, uma rolagem de 300 px só pode mover os filhos em 150 px, dependendo dos valores de perspective e translateZ escolhidos. Se um elemento tiver um valor translateZ de 0, ele será rolado na proporção 1:1 (como costumava ser), mas um filho empurrado em Z para longe da origem da perspectiva será rolado com uma taxa diferente. Resultado: movimento de paralaxe. E, mais importante, isso é tratado automaticamente como parte do mecanismo de rolagem interno do navegador, o que significa que não é necessário ouvir eventos scroll ou alterar background-position.

Uma mosca na pomada: Mobile Safari

Há ressalvas para cada efeito, 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", ou seja, o efeito será perdido.

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

No HTML acima, .parallax-container é novo e nivela efetivamente 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 todos os efeitos 3D (como o valor da perspectiva) que foram aplicados mais acima na árvore.

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

No caso do Mobile Safari, no entanto, as coisas são um pouco mais complicadas. A aplicação de overflow-y: scroll ao elemento de contêiner funciona tecnicamente, mas ao custo de deslizar o elemento de rolagem. A solução é adicionar -webkit-overflow-scrolling: touch, mas isso também nivelará o perspective e não haverá paralaxe.

Do ponto de vista do aprimoramento progressivo, isso provavelmente não é um problema muito. Se não pudermos usar o efeito 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á uma ajuda na forma de position: sticky, que existe para permitir que os elementos "fiquem" na parte de cima da janela de visualização ou de 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 à primeira vista, mas um ponto importante dessa sentença é quando ela se refere exatamente a como a adesão 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 foi calculado em 300 px, há uma nova oportunidade de usar perspectivas (ou qualquer outra transformação) para manipular o valor de deslocamento de 300 px antes de ser aplicado a qualquer elemento fixo.

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 nesse 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 paralaxe do Mobile Safari, o que é uma excelente notícia.

Ressalvas de posicionamento fixas

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

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

Se tudo isso parece um pouco abstrato, consulte 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 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 acontece com qualquer coisa, ainda há protuberâncias e protuberâncias que precisam ser suavizadas:

  • O suporte fixo é inconsistente. O suporte ainda está sendo implementado no Chrome, o Edge não oferece suporte total e o Firefox tem pintura de bugs quando aderente é combinado com transformações de perspectiva (link em inglês). 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 é bom, 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 isso parece trocar 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, você vai ver barras de rolagem e similares, mesmo que o elemento esteja em 1x depois que a perspective tiver sido aplicada. É possível contornar esse problema dimensionando elementos no 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 rolável. As regiões roláveis nunca permitem que você veja ou role até o conteúdo na região negativa.

Conclusão

O paralaxe é um efeito divertido quando usado com cuidado. Como você pode ver, é possível implementá-lo de uma forma que seja eficiente, com acoplamento de rolagem e entre navegadores. Como é necessário um pouco de distorção matemática e uma pequena quantidade de boilerplate para conseguir o efeito desejado, reunimos uma pequena biblioteca auxiliar e exemplo, que podem ser encontrados no nosso repositório de exemplos de elementos da interface no GitHub (link em inglês).

Curta o jogo e conte para nós como foi o processo.