Parallaxe

Introduction

Les sites avec parallaxe sont très à la mode ces derniers temps. En voici quelques exemples:

Si vous ne les connaissez pas, il s'agit de sites dont la structure visuelle de la page change à mesure que vous faites défiler la page. En règle générale, les éléments de la page sont mis à l'échelle, pivotent ou se déplacent proportionnellement à la position de défilement de la page.

Page de démonstration de la parallaxe
Notre page de démonstration avec effet de parallaxe

Que vous aimiez ou non les sites avec parallaxe est une chose, mais vous pouvez affirmer avec une grande certitude qu'ils sont un gouffre de performances. En effet, les navigateurs sont généralement optimisés pour le cas où de nouveaux contenus apparaissent en haut ou en bas de l'écran lorsque vous faites défiler la page (selon la direction de défilement). De manière générale, les navigateurs fonctionnent mieux lorsque très peu de modifications visuelles sont apportées lors du défilement. Ce n'est rarement le cas pour un site en parallaxe, car de nombreux éléments visuels de grande taille changent sur toute la page, ce qui oblige le navigateur à repeindre l'intégralité de la page.

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

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

Nous avons déjà abordé les performances de 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 utile de le lire si vous ne l'avez pas déjà fait.

La question est donc la suivante : si vous créez un site avec défilement parallaxe, êtes-vous obligé de recourir à des recolorations coûteuses ou existe-t-il d'autres approches que vous pouvez adopter pour maximiser les performances ? Voyons les options qui s'offrent à nous.

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

Il semble que ce soit l'approche par défaut adoptée par la plupart des gens. La page contient de nombreux éléments, et chaque fois qu'un événement de défilement est déclenché, de nombreuses mises à jour visuelles sont effectuées pour les transformer.

Si vous démarrez la chronologie DevTools en mode frame et que vous faites défiler la page, vous remarquerez que des opérations de peinture en plein écran coûteuses sont effectuées. Si vous faites défiler la page plusieurs fois, vous pouvez voir plusieurs événements de défilement dans un seul frame, chacun d'eux déclenchant un travail de mise en page.

Outils de développement Chrome sans événements de défilement débouillés.
Outils de développement affichant de grandes peintures et plusieurs mises en page déclenchées par événement dans un seul frame.

N'oubliez pas que pour atteindre 60 FPS (correspondant au taux de rafraîchissement typique de l'écran de 60 Hz), nous avons un peu plus de 16 ms pour tout faire. Dans cette première version, nous effectuons nos mises à jour visuelles chaque fois que nous recevons un événement de défilement. Toutefois, comme nous l'avons indiqué dans les articles précédents sur les animations plus efficaces avec requestAnimationFrame et les performances de défilement, cela ne coïncide pas avec le calendrier de mise à jour du navigateur. Nous manquons donc des images ou effectuons trop de travail dans chacune d'elles. Cela peut facilement donner à votre site un aspect bancal et peu naturel, ce qui déçoit les utilisateurs et rend les chatons malheureux.

Déplaçons le code de mise à jour de l'événement de défilement vers un rappel requestAnimationFrame et capturons 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 beaucoup. En effet, l'opération de mise en page que nous déclenchons par le défilement n'est pas si coûteuse, mais dans d'autres cas d'utilisation, elle peut l'être. Nous n'effectuons désormais qu'une seule opération de mise en page dans chaque frame.

Outils pour les développeurs Chrome avec événements de défilement débouillés.
Outils de développement affichant de grandes peintures et plusieurs mises en page déclenchées par événement dans un seul frame.

Nous pouvons désormais gérer un ou cent événements de défilement par frame, mais nous ne stockons que la valeur la plus récente à utiliser chaque fois que le rappel requestAnimationFrame s'exécute et effectue nos mises à jour visuelles. L'idée est que vous avez cessé d'essayer de forcer les mises à jour visuelles chaque fois que vous recevez un événement de défilement et que vous demandez au navigateur de vous fournir une fenêtre appropriée pour les effectuer. Vous êtes adorable.

Le principal problème de cette approche, requestAnimationFrame ou non, est que nous avons essentiellement une seule couche pour l'ensemble de la page. En déplaçant ces éléments visuels, nous avons besoin de repeindre de grandes surfaces (et coûteuses). En règle générale, le dessin est une opération bloquante (bien que cela change), ce qui signifie que le navigateur ne peut effectuer aucune autre tâche. Nous dépassons souvent le budget de 16 ms de notre frame, et les choses restent saccadées.

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

Au lieu d'utiliser des positions absolues, nous pouvons appliquer des transformations 3D aux éléments. Dans ce cas, une nouvelle couche est attribuée à chaque élément auquel des transformations 3D sont appliquées. Dans les navigateurs WebKit, cela entraîne souvent également un passage au compositeur matériel. Dans l'option 1, en revanche, nous avions une grande couche pour la page qui devait être repeinte en cas de modification, et toute la peinture et la composition étaient gérées par le processeur.

Avec cette option, les choses sont différentes: nous avons potentiellement une couche pour chaque élément auquel nous appliquons une transformation 3D. À partir de ce point, si nous ne faisons que des transformations sur les éléments, nous n'aurons pas besoin de repeindre la couche. Le GPU pourra alors déplacer les éléments et composer la page finale.

Souvent, les utilisateurs se contentent d'utiliser le hack -webkit-transform: translateZ(0); et voient des améliorations de performances magiques. Bien que cela fonctionne aujourd'hui, il existe des problèmes:

  1. Il n'est pas compatible avec tous les navigateurs.
  2. Il force le navigateur à créer une nouvelle couche pour chaque élément transformé. Un grand nombre de calques peut entraîner d'autres goulots d'étranglement de performances. Utilisez-les donc avec parcimonie.
  3. Il a été désactivé pour certains ports WebKit (quatrième point de la liste).

Si vous choisissez la traduction 3D, soyez prudent, car il s'agit d'une solution temporaire. Dans l'idéal, les caractéristiques de rendu des transformations 2D devraient être similaires à celles des transformations 3D. Les navigateurs évoluent à un rythme phénoménal. Nous devrions donc y parvenir avant cette date.

Enfin, vous devez essayer d'éviter les peintures autant que possible et de simplement déplacer les éléments existants sur la page. Par exemple, il est courant sur les sites avec parallaxe d'utiliser des divs de hauteur fixe et de modifier leur position d'arrière-plan pour obtenir l'effet. Malheureusement, cela signifie que l'élément doit être repeint à chaque passage, ce qui peut avoir un impact sur les performances. Si possible, créez plutôt l'élément (encapsulez-le dans un élément div avec overflow: hidden si nécessaire) et traduisez-le simplement.

Option 3: Utiliser un canevas ou WebGL à position fixe

La dernière option que nous allons envisager consiste à utiliser un canevas à position fixe à l'arrière 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 plusieurs avantages:

  • Nous n'avons plus besoin de beaucoup de travail de composition, car nous n'avons qu'un seul élément, le canevas.
  • Nous avons affaire à un seul bitmap accéléré matériellement.
  • L'API Canvas2D est parfaitement adaptée au type de transformations que nous souhaitons effectuer, ce qui signifie que le développement et la maintenance sont plus faciles à gérer.

L'utilisation d'un élément de canevas nous donne une nouvelle couche, mais il ne s'agit que d'une seule couche, alors que dans l'option 2, nous avons reçu une nouvelle couche pour chaque élément avec une transformation 3D appliquée. Nous avons donc une charge de travail accrue en composant toutes ces couches ensemble. Il s'agit également de la solution la plus compatible à l'heure actuelle, compte tenu des différentes implémentations de transformations entre les navigateurs.


/**
 * 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 fonctionne vraiment lorsque vous travaillez avec de grandes images (ou d'autres éléments pouvant être facilement écrits dans un canevas). Il est certainement plus difficile de gérer de grands blocs de texte, mais selon votre site, cette solution peut s'avérer la plus appropriée. Si vous devez gérer du texte dans le canevas, vous devez utiliser la méthode de l'API fillText, mais cela se fait au détriment de l'accessibilité (vous venez de rasteriser le texte en bitmap) et vous devrez désormais gérer le retour à la ligne et un tas d'autres problèmes. Si vous pouvez l'éviter, faites-le. Vous serez probablement mieux servi en utilisant l'approche de transformation ci-dessus.

Étant donné que nous allons aussi loin que possible, il n'y a aucune raison de supposer que le travail de parallaxe doit être effectué dans un élément de canevas. Si le navigateur est compatible, nous pouvons utiliser WebGL. L'essentiel est que WebGL est l'API qui offre le chemin le plus direct vers la carte graphique. Il est donc le plus susceptible d'atteindre 60 FPS, en particulier si les effets du site sont complexes.

Votre réaction immédiate peut être que WebGL est trop lourd ou qu'il n'est pas omniprésent en termes de compatibilité. Toutefois, si vous utilisez quelque chose comme Three.js, vous pouvez toujours revenir à l'utilisation d'un élément de canevas, et votre code est abstrait de manière cohérente et conviviale. Il suffit d'utiliser Modernizr pour vérifier la compatibilité avec l'API appropriée:

// 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'aimez pas trop ajouter des éléments supplémentaires à la page, vous pouvez toujours utiliser un canevas comme élément d'arrière-plan dans Firefox et les navigateurs basés sur WebKit. Ce n'est évidemment pas toujours le cas. Par conséquent, comme d'habitude, vous devez faire preuve de prudence.

Le choix vous appartient

La principale raison pour laquelle les développeurs utilisent par défaut des éléments positionnés de manière absolue plutôt que l'une des autres options peut simplement être l'ubiquité de la compatibilité. Cette approche est, dans une certaine mesure, illusoire, car les anciens navigateurs ciblés sont susceptibles de fournir une expérience de rendu extrêmement mauvaise. Même dans les navigateurs modernes d'aujourd'hui, l'utilisation d'éléments positionnés de manière absolue n'entraîne pas nécessairement de bonnes performances.

Les transformations, en particulier les transformations 3D, vous permettent de travailler directement avec les éléments DOM et d'obtenir un débit d'images stable. Pour réussir, évitez de peindre partout où vous le pouvez et essayez simplement de déplacer des éléments. N'oubliez pas que la façon dont les navigateurs WebKit créent des calques ne correspond pas nécessairement à celle des autres moteurs de navigateur. Veillez donc à le tester avant de vous engager dans cette solution.

Si vous ne visez que les navigateurs de premier plan et que vous pouvez afficher le site à l'aide de canevas, il s'agit peut-être de la meilleure option. Si vous utilisez Three.js, vous devriez pouvoir changer de moteur de rendu très facilement en fonction de la compatibilité dont vous avez besoin.

Conclusion

Nous avons évalué plusieurs approches pour gérer les sites avec parallaxe, des éléments positionnés de manière absolue à l'utilisation d'un canevas à position fixe. L'implémentation que vous choisirez dépendra bien sûr de ce que vous essayez d'atteindre et de la conception spécifique avec laquelle vous travaillez, mais il est toujours bon de savoir que vous avez le choix.

Et comme toujours, quelle que soit l'approche que vous essayez, ne faites pas de suppositions, testez.