Parallaxe

Introduction

Les sites au format parallaxe ont fait l'unanimité récemment. Découvrez-en un peu plus:

Si vous ne les connaissez pas, ce sont les sites sur lesquels la structure visuelle de la page change au fur et à mesure que vous la faites défiler. Normalement, les éléments de l'échelle de page pivotent ou se déplacent proportionnellement à la position de défilement sur la page.

Une page de démonstration au format parallaxe
Page de démonstration avec effet parallaxe

Que vous aimiez ou non les sites en parallaxe, c'est une chose, mais vous pouvez affirmer qu'il s'agit d'un trou noir sur le plan des performances. En effet, les navigateurs ont tendance à être optimisés pour les nouveaux contenus qui s'affichent en haut ou en bas de l'écran lorsque vous faites défiler l'écran (en fonction de la direction de défilement) et, en général, ils fonctionnent mieux lorsque l'aspect visuel est très faible lors du défilement. Pour un site au format parallaxe, ce qui est rarement le cas, car de nombreux éléments visuels de grande taille changent partout sur la page, ce qui oblige le navigateur à repeindre la page entière.

Il est raisonnable de généraliser un site de parallaxe comme ceci:

  • Éléments d'arrière-plan qui, lorsque vous faites défiler l'écran vers le haut et vers le bas, changent de position, de rotation et d'échelle.
  • Contenu de la page, tel que du texte ou des images plus petites, qui défile de haut en bas

Nous avons déjà abordé les performances du défilement et les moyens d'améliorer la réactivité de votre application. Cet article s'appuie sur ces bases. Il peut donc être intéressant de lire cela si vous ne l'avez pas déjà fait.

La question est donc la suivante : si vous créez un site à défilement parallaxe, avez-vous besoin de repeindre coûteux ou existe-t-il d'autres approches pour maximiser les performances ? Découvrons les différentes possibilités qui s'offrent à vous.

Option 1: utiliser des éléments DOM et des positions absolues

Cette approche semble être l’approche par défaut utilisée par la plupart des gens. La page contient de nombreux éléments. Lorsqu'un événement de défilement est déclenché, plusieurs modifications visuelles sont apportées pour les transformer.

Si vous lancez la timeline des outils de développement en mode frame et que vous la faites défiler, vous remarquerez que des opérations de peinture plein écran coûteuses sont effectuées. Si vous faites beaucoup défiler l'écran, vous verrez peut-être plusieurs événements de défilement dans un seul frame, chacun d'entre eux déclenchant un travail de mise en page.

Outils pour les développeurs Chrome sans événements de défilement annulés
Les outils de développement affichent de grandes peintures et plusieurs mises en page déclenchées par des événements dans un seul frame.

Il est important de garder à l'esprit que pour atteindre 60 FPS (ce qui correspond à la fréquence d'actualisation habituelle des écrans de 60 Hz), nous avons un peu plus de 16 ms pour tout faire. Dans cette première version, nous effectuons des mises à jour visuelles chaque fois que nous obtenons un événement de défilement. Toutefois, comme nous l'avons vu dans des articles précédents sur les animations plus simples et plus moyennes avec requestAnimationFrame et les performances de défilement, cela ne coïncide pas avec le calendrier de mise à jour du navigateur. Il peut donc arriver que nous manquions des images ou que nous fassions trop de travail dans chacune d'elles. Cela pourrait facilement donner une impression saccadée et absurde à votre site, et causer des utilisateurs déçus et des chatons mécontents.

Déplaçons le code de mise à jour de l'événement de défilement vers un rappel requestAnimationFrame, puis captureons simplement la valeur de défilement dans le rappel de l'événement de défilement.

Si vous répétez le test de défilement, vous remarquerez peut-être une légère amélioration, mais pas grand-chose. La raison est que l'opération de mise en page que nous déclenchons par défilement n'est pas si coûteuse, mais dans d'autres cas d'utilisation, cela pourrait vraiment l'être. À présent, nous n'effectuons au moins qu'une seule opération de mise en page dans chaque image.

Outils pour les développeurs Chrome avec événements de défilement annulés
Les outils de développement affichent de grandes peintures et plusieurs mises en page déclenchées par des événements dans un seul frame.

Nous pouvons désormais gérer un ou cent événements de défilement par image, mais il est essentiel de ne stocker que la valeur la plus récente à utiliser lorsque le rappel requestAnimationFrame s'exécute et effectue nos mises à jour visuelles. Vous êtes passé d'une tentative de force d'actualisation visuelle chaque fois que vous recevez un événement de défilement, à la demande que le navigateur vous donne une fenêtre appropriée pour le faire. N'est-ce pas gentil ?

Le principal problème avec cette approche, requestAnimationFrame ou non, est que nous avons essentiellement une couche pour l'ensemble de la page. En déplaçant ces éléments visuels autour, nous avons besoin de repeindre volumineux (et coûteux). En règle générale, le tableau est une opération bloquante (bien que cela soit changement), ce qui signifie que le navigateur ne peut effectuer aucune autre tâche et que nous dépassons souvent le budget de notre frame de 16 ms et que l'image reste saccadée.

Option 2: Utiliser des éléments DOM et des transformations 3D

Au lieu d'utiliser les positions absolues, vous pouvez appliquer des transformations 3D aux éléments. Dans cette situation, nous constatons que les éléments auxquels les transformations 3D sont appliquées reçoivent une nouvelle couche par élément et, dans les navigateurs WebKit, cela provoque souvent un basculement vers le compositeur matériel. En revanche, avec l'option 1, nous avions une grande couche pour la page qui devait être repeinte en cas de changement, et que le rendu et la composition étaient gérés par le processeur.

Cela signifie qu'avec cette option, les choses sont différentes: nous avons potentiellement une couche pour tout élément auquel nous appliquons une transformation 3D. Si, à partir de là, nous nous contentons d'effectuer davantage de transformations sur les éléments, nous n'aurons pas besoin de repeindre le calque, et le GPU pourra gérer le déplacement des éléments et la composition de la page finale ensemble.

Souvent, les utilisateurs se contentent d'utiliser le hack -webkit-transform: translateZ(0); et constatent des améliorations de performances magiques. Même si cela fonctionne aujourd'hui, cela pose problème:

  1. Il n'est pas compatible avec plusieurs navigateurs.
  2. Il force la main du navigateur en créant une nouvelle couche pour chaque élément transformé. De nombreuses couches peuvent entraîner d'autres goulots d'étranglement. Par conséquent, utilisez-les avec parcimonie.
  3. Il a été désactivé pour certains ports WebKit (quatrième puce en partant du bas).

Si vous optez pour la traduction en 3D, soyez prudent, car il s'agit d'une solution temporaire à votre problème. Idéalement, les transformations 2D obtiendraient des caractéristiques de rendu semblables à celles de la 3D. Les navigateurs progressent à un rythme phénoménal. Nous espérons que ce sera d'ici là.

Enfin, vous devez vous efforcer d'éviter les peintures autant que possible et simplement déplacer les éléments existants sur la page. À titre d'exemple, une approche typique sur les sites au format parallaxe consiste à utiliser des éléments div de hauteur fixe et à modifier leur position d'arrière-plan pour obtenir l'effet obtenu. Malheureusement, cela signifie que l'élément doit être repeint à chaque passe, ce qui peut entraîner des coûts en termes de performances. À la place, vous devez, si possible, créer l'élément (en l'encapsulant dans un élément div avec overflow: hidden si nécessaire), puis le traduire.

Option 3: utiliser un canevas à position fixe ou WebGL

La dernière option que nous allons envisager d'utiliser consiste à utiliser un canevas à position fixe au dos de la page, dans lequel nous allons dessiner nos images transformées. À première vue, cette solution ne semble pas être la plus performante, mais elle présente en fait quelques avantages:

  • Nous n'avons plus besoin d'autant de travail du compositeur, car il ne comporte qu'un seul élément, le canevas.
  • Il s'agit d'un seul bitmap avec accélération matérielle.
  • L'API Canvas2D est parfaitement adaptée au type de transformations que nous envisageons d'effectuer, ce qui rend le développement et la maintenance plus faciles à gérer.

L'utilisation d'un élément canevas nous donne un nouveau calque, mais il ne s'agit que d'une couche. En revanche, l'option 2 nous a donné un nouveau calque pour chaque élément avec une transformation 3D appliquée. La composition de toutes ces couches ensemble a donc augmenté la charge de travail. Il s'agit également de la solution la plus compatible aujourd'hui compte tenu des différentes implémentations de transformations multinavigateurs.


/**
 * Updates and draws in the underlying visual elements to the canvas.
 */
function updateElements () {

  var relativeY = lastScrollY / h;

  // Fill the canvas up
  context.fillStyle = "#1e2124";
  context.fillRect(0, 0, canvas.width, canvas.height);

  // Draw the background
  context.drawImage(bg, 0, pos(0, -3600, relativeY, 0));

  // Draw each of the blobs in turn
  context.drawImage(blob1, 484, pos(254, -4400, relativeY, 0));
  context.drawImage(blob2, 84, pos(954, -5400, relativeY, 0));
  context.drawImage(blob3, 584, pos(1054, -3900, relativeY, 0));
  context.drawImage(blob4, 44, pos(1400, -6900, relativeY, 0));
  context.drawImage(blob5, -40, pos(1730, -5900, relativeY, 0));
  context.drawImage(blob6, 325, pos(2860, -7900, relativeY, 0));
  context.drawImage(blob7, 725, pos(2550, -4900, relativeY, 0));
  context.drawImage(blob8, 570, pos(2300, -3700, relativeY, 0));
  context.drawImage(blob9, 640, pos(3700, -9000, relativeY, 0));

  // Allow another rAF call to be scheduled
  ticking = false;
}

/**
 * Calculates a relative disposition given the page's scroll
 * range normalized from 0 to 1
 * @param {number} base The starting value.
 * @param {number} range The amount of pixels it can move.
 * @param {number} relY The normalized scroll value.
 * @param {number} offset A base normalized value from which to start the scroll behavior.
 * @returns {number} The updated position value.
 */
function pos(base, range, relY, offset) {
  return base + limit(0, 1, relY - offset) * range;
}

/**
 * Clamps a number to a range.
 * @param {number} min The minimum value.
 * @param {number} max The maximum value.
 * @param {number} value The value to limit.
 * @returns {number} The clamped value.
 */
function limit(min, max, value) {
  return Math.max(min, Math.min(max, value));
}

Cette approche est efficace lorsqu'il s'agit de grandes images (ou d'autres éléments qui peuvent être facilement écrits sur un canevas). Bien entendu, traiter de grands blocs de texte serait plus difficile, mais en fonction de votre site, cette approche peut s'avérer être la solution la plus appropriée. Si vous devez gérer du texte dans le canevas, vous devrez utiliser la méthode d'API fillText, mais cela s'avérera au détriment de l'accessibilité (vous venez de rastériser le texte en bitmap) et vous devrez maintenant gérer les renvois à la ligne et de nombreux autres problèmes. Si vous pouvez l'éviter, c'est le cas. L'approche de transformation décrite ci-dessus vous sera probablement plus utile.

Étant donné que nous faisons le plus loin possible, il n'y a aucune raison de présumer que le travail en parallaxe doit être effectué dans un élément de canevas. Si le navigateur le permet, nous pouvons utiliser WebGL. La clé ici est que WebGL a le chemin le plus direct de toutes les API vers la carte graphique. Par conséquent, il est le plus probable qu'il atteigne 60 FPS, surtout si les effets du site sont complexes.

Votre réaction immédiate peut être que WebGL est trop gourmand ou qu'il n'est pas omniprésent en termes de compatibilité, mais si vous utilisez Three.js, vous pouvez toujours revenir à l'utilisation d'un élément de canevas, et votre code est extrait de manière cohérente et amicale. Il nous suffit d'utiliser Modernizr pour vérifier la compatibilité des API:

// check for WebGL support, otherwise switch to canvas
if (Modernizr.webgl) {
  renderer = new THREE.WebGLRenderer();
} else if (Modernizr.canvas) {
  renderer = new THREE.CanvasRenderer();
}

Pour conclure, si vous n'êtes pas fan de l'ajout d'éléments supplémentaires à la page, vous pouvez toujours utiliser un canevas comme élément d'arrière-plan dans Firefox et dans les navigateurs WebKit. Cette pratique n'est pas omniprésente, de toute évidence. Alors, comme d'habitude, vous devez la traiter avec précaution.

Le choix vous appartient

La principale raison pour laquelle les développeurs utilisent par défaut des éléments positionnés de façon absolue plutôt que n'importe quelle autre option est peut-être simplement l'omniprésence de l'assistance. Ce phénomène est, dans une certaine mesure, illusoire, car les anciens navigateurs ciblés risquent de proposer un affichage extrêmement médiocre. Même dans les navigateurs récents d'aujourd'hui, l'utilisation d'éléments parfaitement positionnés n'entraîne pas nécessairement de bonnes performances.

Les transformations, certainement en 3D, vous permettent de travailler directement avec des éléments DOM et d'obtenir une fréquence d'images stable. La clé du succès est ici d'éviter de peindre partout où vous le pouvez et simplement d'essayer de déplacer des éléments. Gardez à l'esprit que la façon dont les navigateurs WebKit créent des calques n'a pas nécessairement de lien avec d'autres moteurs de navigateur. Veillez donc à la tester avant de vous engager dans cette solution.

Si votre objectif est limité aux navigateurs et que vous êtes en mesure d'afficher votre site à l'aide de canevas, cette option est sans doute la plus adaptée. En effet, si vous utilisiez Three.js, vous devriez pouvoir passer d'un moteur de rendu à un autre très facilement en fonction de la compatibilité dont vous avez besoin.

Conclusion

Nous avons étudié différentes approches pour gérer les sites au parallaxe, des éléments positionnés de façon absolue à l'utilisation d'un canevas à position fixe. L’implémentation que vous adoptez dépendra, bien sûr, de ce que vous essayez d’accomplir et de la conception spécifique avec laquelle vous travaillez, mais il est toujours bon de savoir que vous avez des options.

Et comme toujours, quelle que soit l'approche que vous essayez: ne la devinez pas, testez-la.