Parallaxe performant

Robert Flack
Robert Flack

Que vous l'aimiez ou que vous le détestiez, le parallaxe est là pour durer. Lorsqu'il est utilisé judicieusement, il peut ajouter de la profondeur et de la subtilité à une application Web. Le problème, cependant, est que l'implémentation du parallaxe de manière performante peut s'avérer difficile. Dans cet article, nous allons examiner une solution à la fois performante et, tout aussi important, compatible avec tous les navigateurs.

Illustration de la parallaxe.

Résumé

  • N'utilisez pas d'événements de défilement ni background-position pour créer des animations de parallaxe.
  • Utilisez les transformations CSS 3D pour créer un effet de parallaxe plus précis.
  • Pour Safari mobile, utilisez position: sticky pour vous assurer que l'effet de parallaxe est propagé.

Si vous souhaitez une solution prête à l'emploi, accédez au dépôt GitHub des exemples d'éléments d'interface utilisateur et récupérez le compagnon JavaScript de parallaxe. Vous pouvez voir une démonstration en direct du défilement en parallaxe dans le dépôt GitHub.

Parallélisme des problèmes

Pour commencer, examinons deux méthodes courantes pour obtenir un effet de parallaxe et, en particulier, pourquoi elles ne sont pas adaptées à nos besoins.

Mauvaise pratique: utiliser des événements de défilement

L'exigence clé de la parallaxe est qu'elle doit être couplée au défilement. Pour chaque modification de la position de défilement de la page, la position de l'élément de parallaxe doit être mise à jour. Bien que cela semble simple, un mécanisme important des navigateurs modernes est leur capacité à fonctionner de manière asynchrone. Dans notre cas, cela s'applique aux événements de défilement. Dans la plupart des navigateurs, les événements de défilement sont diffusés de manière "optimisée" et ne sont pas garantis pour être diffusés sur chaque frame de l'animation de défilement.

Cette information importante nous indique pourquoi nous devons éviter une solution basée sur JavaScript qui déplace des éléments en fonction des événements de défilement : JavaScript ne garantit pas que la parallaxe sera synchronisée avec la position de défilement de la page. Dans les anciennes versions de Mobile Safari, les événements de défilement étaient en fait envoyés à la fin du défilement, ce qui rendait impossible la création d'un effet de défilement basé sur JavaScript. Les versions plus récentes envoient des événements de défilement pendant l'animation, mais, comme Chrome, de manière "à la meilleure approximation". Si le thread principal est occupé par un autre travail, les événements de défilement ne seront pas envoyés immédiatement, ce qui signifie que l'effet de parallaxe sera perdu.

Erreur: mise à jour de background-position

Nous souhaitons également éviter de peindre sur chaque frame. De nombreuses solutions tentent de modifier background-position pour obtenir l'effet parallaxe, ce qui oblige le navigateur à repeindre les parties concernées de la page lors du défilement. Cela peut être assez coûteux pour perturber considérablement l'animation.

Si nous voulons tenir la promesse du mouvement parallaxe, nous voulons quelque chose qui puisse être appliqué en tant que propriété accélérée (ce qui signifie aujourd'hui s'en tenir aux transformations et à l'opacité) et qui ne repose pas sur des événements de défilement.

CSS en 3D

Scott Kellum et Keith Clark ont effectué un travail important dans le domaine de l'utilisation du CSS 3D pour obtenir un mouvement de parallaxe. La technique qu'ils utilisent est la suivante:

  • Configurez un élément contenant pour faire défiler avec overflow-y: scroll (et probablement overflow-x: hidden).
  • Appliquez à ce même élément une valeur perspective et un perspective-origin défini sur top left ou 0 0.
  • Appliquez une translation sur l'axe Z aux enfants de cet élément, puis réduisez leur échelle pour créer un mouvement de parallaxe sans affecter leur taille à l'écran.

Le CSS de cette approche se présente comme suit:

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

Ce qui suppose un extrait de code HTML comme suit:

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

Ajuster l'échelle pour la perspective

Si vous repoussez l'élément enfant, il rétrécit proportionnellement à la valeur de perspective. Vous pouvez calculer la valeur à laquelle il doit être mis à l'échelle à l'aide de l'équation suivante: (perspective - distance) / perspective. Comme nous voulons probablement que l'élément en parallaxe soit en parallaxe, mais qu'il apparaisse à la taille que nous avons créée, il doit être mis à l'échelle de cette manière, plutôt que de rester tel quel.

Dans le cas du code ci-dessus, la perspective est de 1 px et la distance Z de parallax-child est de -2 px. Cela signifie que l'élément doit être triplé, ce qui correspond à la valeur insérée dans le code : scale(3).

Pour tout contenu auquel aucune valeur translateZ n'est appliquée, vous pouvez remplacer cette valeur par zéro. Cela signifie que l'échelle est (perspective - 0) / perspective, ce qui donne une valeur de 1, ce qui signifie qu'elle n'a pas été mise à l'échelle ni à la hausse ni à la baisse. C'est très pratique.

Fonctionnement de cette approche

Il est important de comprendre pourquoi cela fonctionne, car nous allons utiliser ces connaissances sous peu. Le défilement est en fait une transformation, c'est pourquoi il peut être accéléré. Il implique principalement de déplacer des calques avec le GPU. Dans un défilement typique, qui est un défilement sans aucune notion de perspective, le défilement se produit de manière proportionnelle lorsque l'on compare l'élément de défilement et ses enfants. Si vous faites défiler un élément vers le bas de 300px, ses enfants sont transformés vers le haut de la même quantité: 300px.

Toutefois, l'application d'une valeur de perspective à l'élément de défilement perturbe ce processus. Elle modifie les matrices qui sous-tendent la transformation de défilement. Désormais, un défilement de 300 px ne peut déplacer les enfants que de 150 px, en fonction des valeurs perspective et translateZ que vous avez choisies. Si la valeur translateZ d'un élément est de 0, il sera mis à l'échelle 1:1 (comme auparavant), mais un enfant éloigné de l'origine de la perspective en Z sera mis à l'échelle à un autre taux. Résultat net: mouvement parallaxe. Et, surtout, cela est géré automatiquement dans le mécanisme de défilement interne du navigateur, ce qui signifie qu'il n'est pas nécessaire d'écouter les événements scroll ni de modifier background-position.

Un point négatif: Safari mobile

Chaque effet comporte des mises en garde, et l'une des plus importantes pour les transformations concerne la préservation des effets 3D pour les éléments enfants. Si la hiérarchie contient des éléments entre l'élément avec une perspective et ses enfants en parallaxe, la perspective 3D est "aplatie", ce qui signifie que l'effet est perdu.

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

Dans le code HTML ci-dessus, .parallax-container est nouveau. Il aplatitra efficacement la valeur perspective, et nous perdrons l'effet de parallaxe. Dans la plupart des cas, la solution est assez simple: vous ajoutez transform-style: preserve-3d à l'élément, ce qui lui permet de propager les effets 3D (comme notre valeur de perspective) qui ont été appliqués plus haut dans l'arborescence.

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

Toutefois, dans le cas de Safari mobile, les choses sont un peu plus complexes. Appliquer overflow-y: scroll à l'élément de conteneur fonctionne techniquement, mais au prix de la possibilité de lancer l'élément de défilement. La solution consiste à ajouter -webkit-overflow-scrolling: touch, mais cela aplatit également perspective et nous n'obtenons aucune parallaxe.

Du point de vue de l'amélioration progressive, ce n'est probablement pas un problème majeur. Si nous ne pouvons pas utiliser la parallaxe dans toutes les situations, notre application fonctionnera toujours, mais il serait préférable de trouver une solution de contournement.

position: sticky à la rescousse !

Il existe en fait une aide sous la forme de position: sticky, qui permet aux éléments de "coller" en haut du viewport ou d'un élément parent donné lors du défilement. Comme la plupart d'entre elles, la spécification est assez volumineuse, mais elle contient un petit bijou utile:

Cela peut sembler anodin à première vue, mais un point clé de cette phrase est lorsqu'elle fait référence à la façon exacte dont l'adhérence d'un élément est calculée: "le décalage est calculé par rapport à l'ancêtre le plus proche avec une zone de défilement". En d'autres termes, la distance à déplacer l'élément persistant (pour qu'il apparaisse attaché à un autre élément ou au viewport) est calculée avant l'application d'autres transformations, et non après. Cela signifie que, tout comme dans l'exemple de défilement précédent, si le décalage a été calculé à 300 px, il est possible d'utiliser des perspectives (ou toute autre transformation) pour manipuler cette valeur de décalage de 300 px avant qu'elle ne soit appliquée à des éléments persistants.

En appliquant position: -webkit-sticky à l'élément en parallaxe, nous pouvons "inverser" efficacement l'effet d'aplatissement de -webkit-overflow-scrolling: touch. Cela garantit que l'élément de parallaxe fait référence à l'ancêtre le plus proche avec une zone de défilement, qui est .container dans ce cas. Ensuite, comme précédemment, .parallax-container applique une valeur perspective, ce qui modifie le décalage de défilement calculé et crée un effet de parallaxe.

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

Cela restaure l'effet de parallaxe pour Mobile Safari, ce qui est une excellente nouvelle.

Mises en garde concernant le positionnement persistant

Il existe toutefois une différence: position: sticky modifie la mécanique de parallaxe. Le positionnement persistant tente de coller l'élément au conteneur de défilement, contrairement à une version non persistante. Cela signifie que la parallaxe avec les éléments persistants finit par être l'inverse de celle sans eux:

  • Avec position: sticky, plus l'élément est proche de z=0, moins il se déplace.
  • Sans position: sticky, plus l'élément est proche de z=0, plus il se déplace.

Si tout cela vous semble un peu abstrait, regardez cette démonstration de Robert Flack, qui montre comment les éléments se comportent différemment avec et sans positionnement persistant. Pour voir la différence, vous avez besoin de Chrome Canary (version 56 au moment de la rédaction de cet article) ou de Safari.

Capture d&#39;écran de la perspective parallaxe

Démonstration par Robert Flack montrant comment position: sticky affecte le défilement parallaxe.

Divers bugs et solutions de contournement

Cependant, comme pour tout, il reste des bosses et des aspérités à lisser:

  • La prise en charge des éléments persistants est incohérente. La prise en charge est toujours en cours d'implémentation dans Chrome, Edge n'est pas compatible du tout et Firefox présente des bugs de peinture lorsque la fonction sticky est combinée à des transformations de perspective. Dans ce cas, il est utile d'ajouter un peu de code pour n'ajouter position: sticky (la version avec le préfixe -webkit-) que lorsque cela est nécessaire, ce qui est uniquement pour Safari mobile.
  • L'effet ne fonctionne pas "tout simplement" dans Edge. Edge tente de gérer le défilement au niveau du système d'exploitation, ce qui est généralement une bonne chose, mais dans ce cas, cela l'empêche de détecter les changements de perspective pendant le défilement. Pour résoudre ce problème, vous pouvez ajouter un élément de position fixe, car cela semble passer Edge à une méthode de défilement non-OS et garantit qu'il tient compte des changements de perspective.
  • "Le contenu de la page est devenu énorme !" De nombreux navigateurs tiennent compte de l'échelle pour déterminer la taille du contenu de la page, mais malheureusement, Chrome et Safari ne tiennent pas compte de la perspective. Par conséquent, si une échelle de 3 x est appliquée à un élément, vous pouvez voir des barres de défilement et d'autres éléments, même si l'élément est à 1 x après l'application de perspective. Il est possible de contourner ce problème en effectuant une mise à l'échelle des éléments à partir du coin inférieur droit (avec transform-origin: bottom right). Cela fonctionne, car les éléments surdimensionnés se développeront dans la "région négative" (généralement en haut à gauche) de la zone de défilement. Les régions de défilement ne vous permettent jamais de voir ni de faire défiler le contenu dans la région négative.

Conclusion

La parallaxe est un effet amusant lorsqu'il est utilisé de manière réfléchie. Comme vous pouvez le constater, il est possible de l'implémenter de manière performante, couplée au défilement et multinavigateur. Comme il nécessite un peu de gymnastique mathématique et une petite quantité de code standard pour obtenir l'effet souhaité, nous avons créé une petite bibliothèque d'aide et un exemple, que vous trouverez dans notre dépôt GitHub d'exemples d'éléments d'interface utilisateur.

Essayez-la et dites-nous ce que vous en pensez.