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 des transformations 3D CSS 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 utiliser la solution intégrée, accédez au dépôt GitHub d'exemples d'éléments d'interface utilisateur et récupérez le fichier JS de l'outil d'aide parallaxe. Vous pouvez voir une démonstration en direct du défilement en parallaxe dans le dépôt GitHub.

Parallaxes problématiques

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 fournis dans la mesure du possible et ne sont pas nécessairement diffusés à chaque image 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 présentent des événements de défilement pendant l'animation, mais, comme pour Chrome, laissent "le mieux possible". 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.

Mauvais: 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 beaucoup travaillé sur l'utilisation de la 3D CSS pour obtenir un mouvement de parallaxe. Voici la technique qu'ils utilisent efficacement:

  • 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 utilisé pour 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 1px et la distance Z de parallax-child est de -2px. 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 une valeur de 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 d'expliquer pourquoi cela fonctionne, car nous allons bientôt nous en servir. 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.

Cependant, l'application d'une valeur de perspective à l'élément de défilement perturbe ce processus ; cela modifie les matrices sous-jacentes à la transformation de défilement. Désormais, un défilement de 300 pixels ne peut déplacer les enfants que de 150 pixels, 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 par la machine 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 aplatira efficacement la valeur perspective, ce qui entraînera la perte de 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 aplatira également perspective et n'entraînera aucun parallaxe.

Du point de vue de l'amélioration progressive, ce n'est probablement pas un problème majeur. Si nous ne pouvons pas parallaxe dans toutes les situations, notre application fonctionnera tout de même, mais il serait bon 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 ne semble peut-être pas signifier grand-chose à première vue, mais il s'agit d'un point clé lorsqu'il s'agit de savoir comment, exactement, la rétention d'un élément est calculée: "le décalage est calculé en fonction de 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 pixels, il est possible d'utiliser des perspectives (ou toute autre transformation) pour manipuler cette valeur de décalage de 300 pixels avant de l'appliquer aux é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, 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 dans le conteneur à faire défiler, contrairement à une version non persistante. Cela signifie que le parallaxe avec des notes persistantes finit par être l'inverse de celui qui n'en est pas:

  • 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

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

  • L'assistance persistante 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 lors du 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 ma page est devenu énorme !" De nombreux navigateurs tiennent compte de cette échelle pour déterminer la taille du contenu d'une page, mais, malheureusement, Chrome et Safari ne tiennent pas compte de la perspective. Ainsi, si une échelle de 3 fois est appliquée à un élément, des barres de défilement et d'autres éléments similaires peuvent s'afficher, même si l'élément est à 1x 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.

Jouez et dites-nous comment vous vous en sortez.