Parallaxin'

Paul Lewis

Einleitung

Parallaxen-Websites sind in letzter Zeit sehr beliebt, sehen Sie sich einfach diese an:

Falls Sie sie noch nicht kennen, sind sie die Websites, auf denen sich die visuelle Struktur der Seite ändert, wenn Sie scrollen. Normalerweise werden Elemente auf der Seite proportional zur Scrollposition auf der Seite skaliert oder bewegt.

Eine Demoseite für Parallaxe
Unsere Demoseite mit Parallaxe-Effekt

Ob Sie Websites mit Parallaxeneffekten mögen oder nicht, ist eine Sache, Sie können aber mit ziemlicher Sicherheit sagen, dass sie eine schwarze Lücke sind. Der Grund dafür ist, dass Browser tendenziell für den Fall optimiert sind, dass beim Scrollen (abhängig von der Scrollrichtung) neuer Inhalt oben oder unten auf dem Bildschirm erscheint. Im Allgemeinen funktionieren die Browser am besten, wenn sich während des Scrollens nur wenige visuelle Änderungen ergeben. Bei einer Parallaxe-Website ist dies selten der Fall, da sich häufig große visuelle Elemente auf der gesamten Seite ändern, sodass der Browser die gesamte Seite aktualisiert.

Für eine Parallaxe-Website ist beispielsweise Folgendes zu verallgemeinern:

  • Hintergrundelemente, die ihre Position, Drehung und Skalierung ändern, wenn Sie nach oben und unten scrollen.
  • Seiteninhalte wie Text oder kleinere Bilder, die wie von oben nach unten gescrollt werden können.

Wir haben bereits über die Scrollleistung und die Möglichkeiten zur Verbesserung der Reaktionsgeschwindigkeit Ihrer App gesprochen. Dieser Artikel baut auf dieser Grundlage auf. Vielleicht lohnt es sich, dies zu lesen, falls Sie das noch nicht getan haben.

Die Frage ist also, ob Sie beim Erstellen einer Website mit Parallaxe-Bildung an teure Überarbeitungen gebunden sind oder gibt es alternative Ansätze, mit denen Sie die Leistung maximieren können? Sehen wir uns die Optionen an.

Option 1: DOM-Elemente und absolute Positionen verwenden

Dies scheint der Standardansatz zu sein, den die meisten Menschen verfolgen. Die Seite enthält eine Reihe von Elementen, und jedes Mal, wenn ein Scroll-Ereignis ausgelöst wird, werden eine Reihe von visuellen Aktualisierungen durchgeführt, um diese zu transformieren.

Wenn Sie die Zeitachse der Entwicklertools im Framemodus starten und umher scrollen, werden Sie feststellen, dass es teure Vollbild- übertragene Vorgänge gibt. Und wenn Sie viel scrollen, sehen Sie möglicherweise mehrere Scroll-Ereignisse in einem einzelnen Frame, die alle Layoutarbeiten auslösen.

Chrome-Entwicklertools ohne entsprungene Scroll-Ereignisse.
Entwicklertools mit großen Farben und mehreren ereignisbasierten Layouts in einem einzigen Frame.

Wichtig ist: Bei 60 fps (ähnlich der typischen Aktualisierungsrate von 60 Hz) bleiben etwas mehr als 16 ms, um alles zu schaffen. In dieser ersten Version führen wir jedes Mal ein visuelles Aktualisierungen durch, wenn wir ein Scroll-Ereignis erhalten. Aber wie wir in früheren Artikeln über schlankere, geläufigere Animationen mit requestAnimationFrame und Scrollleistung besprochen haben, stimmt dies nicht mit dem Aktualisierungszeitplan des Browsers überein. Daher verpassen wir entweder Frames oder erledigen zu viel Arbeit in jedem einzelnen Frame. Dies könnte zu einer unnatürlichen, unnatürlichen Atmosphäre auf Ihrer Website führen, die zu enttäuschten Nutzern und unglücklichen Kätzchen führt.

Wir verschieben den Aktualisierungscode aus dem Scroll-Ereignis in einen requestAnimationFrame-Callback und erfassen einfach den Scroll-Wert im Callback des Scroll-Ereignisses.

Wenn Sie den Scroll-Test wiederholen, werden Sie vermutlich eine leichte Verbesserung feststellen, wenn auch nicht viel. Der Grund dafür ist, dass der Layoutvorgang, den wir durch Scrollen auslösen, nicht allzu teuer ist. In anderen Anwendungsfällen könnte er das sein. Jetzt führen wir zumindest nur einen Layoutvorgang in jedem Frame aus.

Chrome-Entwicklertools mit entschlüsselten Scroll-Ereignissen
Entwicklertools mit großen Farben und mehreren ereignisbasierten Layouts in einem einzigen Frame.

Wir können jetzt ein oder hundert Scroll-Ereignisse pro Frame verarbeiten, aber entscheidend ist, dass wir nur den neuesten Wert speichern, der verwendet wird, wenn der requestAnimationFrame-Callback ausgeführt wird und unsere visuellen Aktualisierungen durchgeführt werden. Der Punkt besteht darin, dass Sie nicht mehr jedes Mal versucht haben, visuelle Aktualisierungen zu erzwingen, wenn Sie ein Scroll-Ereignis erhalten, sondern Sie anfordern, dass der Browser Ihnen ein entsprechendes Fenster zur Verfügung stellt. Bist du nicht süß?

Das Hauptproblem bei diesem Ansatz, ob requestAnimationFrame oder nicht, besteht darin, dass wir im Wesentlichen eine Ebene für die gesamte Seite haben und dass wir diese visuellen Elemente verschieben müssen, um große (und teure) Darstellungen zu erstellen. Normalerweise ist das Painting ein blockierender Vorgang, was jedoch sich ändert. Dies bedeutet, dass der Browser keine weiteren Aufgaben erledigen kann und wir häufig das Budget für unseren Frame von 16 ms überschreiten und es trotzdem Verzögerungen gibt.

Option 2: DOM-Elemente und 3D-Transformationen verwenden

Statt absoluten Positionen zu verwenden, können wir auch 3D-Transformationen auf die Elemente anwenden. In diesem Fall wird den Elementen mit den angewendeten 3D-Transformationen eine neue Ebene pro Element zugewiesen. In WebKit-Browsern führt dies häufig auch zu einem Wechsel zum Hardwarecompositor. In Option 1 hingegen hatten wir eine große Ebene für die Seite, die neu gezeichnet werden musste, wenn sich etwas änderte und die gesamte Bild- und Zusammenstellung über die CPU erfolgte.

Das bedeutet, dass bei dieser Option alles anders ist: Es gibt potenziell eine Ebene für jedes Element, auf das wir eine 3D-Transformation anwenden. Wenn wir von diesem Punkt an nur noch mehr Transformationen an den Elementen durchführen, müssen wir die Ebene nicht neu malen und die GPU kann die Elemente verschieben und die endgültige Seite zusammensetzen.

Oft wird mit dem -webkit-transform: translateZ(0);-Hack eine Leistungssteigerung erzielt, und obwohl das heute funktioniert, gibt es doch einige Probleme:

  1. Es ist nicht browserübergreifend kompatibel.
  2. Der Browser wird gezwungen, für jedes transformierte Element eine neue Ebene zu erstellen. Viele Ebenen können zu Leistungsengpässen führen, deshalb sollten Sie sie sparsam einsetzen.
  3. Sie wurde für einige WebKit-Ports deaktiviert (vierter Aufzählungspunkt von unten).

Seien Sie vorsichtig, wenn Sie die 3D-Übersetzung ausführen, denn dies ist nur eine vorübergehende Lösung für Ihr Problem! Im Idealfall sehen wir bei 2D-Transformationen ähnliche Rendering-Eigenschaften wie bei 3D-Modellen. Browser entwickeln sich in rasantem Tempo weiter.

Versuchen Sie schließlich, Painting-Elemente wo immer möglich zu vermeiden und einfach vorhandene Elemente auf der Seite zu verschieben. Bei Parallaxe-Websites ist es beispielsweise üblich, div-Elemente mit fester Höhe zu verwenden und ihre Hintergrundposition zu ändern, um den Effekt zu erzielen. Leider bedeutet dies, dass das Element bei jedem Durchlauf neu gezeichnet werden muss, was Kosten in puncto Leistung verursachen kann. Stattdessen sollten Sie das Element nach Möglichkeit erstellen (es gegebenenfalls mit overflow: hidden in ein div-Element umschließen) und es einfach übersetzen.

Option 3: Canvas mit fester Position oder WebGL verwenden

Die letzte Option, die wir in Betracht ziehen, ist die Verwendung eines Canvas mit fester Position am Ende der Seite, in das wir die transformierten Bilder zeichnen. Auf den ersten Blick scheint dies nicht die leistungsstärkste Lösung zu sein, hat aber in Wirklichkeit einige Vorteile dieses Ansatzes:

  • Wir benötigen nicht mehr so viel Arbeit beim Komponieren, da es nur noch ein Element, den Canvas, gibt.
  • Wir haben es praktisch mit einer einzelnen hardwarebeschleunigten Bitmap zu tun.
  • Die Canvas2D API eignet sich hervorragend für die Art von Transformationen, die wir durchführen möchten, was Entwicklung und Wartung überschaubarer macht.

Durch die Verwendung eines Canvas-Elements erhalten wir eine neue Ebene, die aber nur eine Ebene umfasst. Bei Option 2 hingegen erhielten wir eine neue Ebene für jedes Element mit einer angewendeten 3D-Transformation. Die Arbeitslast für die Zusammensetzung all dieser Ebenen steigt also. Angesichts der unterschiedlichen browserübergreifenden Implementierungen von Transformationen ist dies auch die aktuellste Lösung.


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

Dieser Ansatz funktioniert wirklich, wenn Sie mit großen Bildern (oder anderen Elementen, die sich leicht auf eine Leinwand schreiben lassen) zu tun haben und der Umgang mit großen Textblöcken auf jeden Fall schwieriger wäre, aber je nach Ihrer Website kann es sich als die beste Lösung erweisen. Wenn Sie mit Text im Canvas arbeiten müssen, müssen Sie die API-Methode fillText verwenden, allerdings geht dies auf Kosten der Zugänglichkeit (Sie haben den Text einfach als Bitmap gerastert!) und Sie müssen sich nun mit Zeilenumbruch und einer ganzen Reihe anderer Probleme auseinandersetzen. Wenn Sie es vermeiden können, sollten Sie das auch wirklich vermeiden. Mit dem oben beschriebenen Ansatz würden Sie wahrscheinlich besser profitieren.

Da wir dies so weit wie möglich unternehmen, gibt es keinen Grund zur Annahme, dass die Arbeit mit dem Parallaxe innerhalb eines Canvas-Elements erfolgen sollte. Wenn der Browser dies unterstützt, könnten wir WebGL verwenden. Entscheidend ist hierbei, dass WebGL den direktsten Weg aller APIs zur Grafikkarte hat und daher am ehesten geeignet ist, um 60 fps zu erreichen, insbesondere wenn die Effekte der Website komplex sind.

Ihre unmittelbare Reaktion könnte sein, dass WebGL Overkill oder keine allgegenwärtige Unterstützung ist. Wenn Sie jedoch Three.js verwenden, können Sie jederzeit auf ein Canvas-Element zurückgreifen und Ihr Code wird einheitlich und freundlich abstrahiert. Wir müssen lediglich mit Modernizr prüfen, ob die richtige API-Unterstützung vorhanden ist:

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

Noch ein letzter Gedanke bei diesem Ansatz: Wenn Sie kein Fan von zusätzlichen Elementen zu der Seite sind, können Sie sowohl in Firefox- als auch in WebKit-basierten Browsern ein Canvas als Hintergrundelement verwenden. Das ist natürlich nicht allgegenwärtig. Sie sollten also wie immer mit Vorsicht vorgehen.

Die Entscheidung liegt ganz bei dir

Der Hauptgrund dafür, dass Entwickler vorrangig an absolut positionierte Elemente arbeiten und keine der anderen Optionen verwenden, kann einfach die Allgegenwärtigkeit des Supports sein. Dies ist zum Teil nur illustrativ, da die älteren Browser, auf die es ausgerichtet ist, wahrscheinlich eine extrem schlechte Rendering-Erfahrung bieten. Selbst in modernen Browsern führt die Verwendung von absolut positionierten Elementen nicht unbedingt zu einer guten Leistung.

Transformationen, sicherlich die 3D-Art, bieten Ihnen die Möglichkeit, direkt mit DOM-Elementen zu arbeiten und eine solide Framerate zu erzielen. Der Schlüssel zum Erfolg besteht hier darin, möglichst nicht zu malen, sondern einfach die Elemente zu verschieben. Bedenken Sie, dass die Art und Weise, wie WebKit-Browser Ebenen erstellt, nicht unbedingt mit anderen Browser-Engines korreliert. Daher sollten Sie es unbedingt testen, bevor Sie sich auf diese Lösung festlegen.

Wenn Sie nur für die wichtigsten Browser anstreben und die Website mithilfe von Canvases rendern können, ist dies wahrscheinlich die beste Option für Sie. Wenn Sie Three.js verwenden, sollten Sie den Renderer je nach gewünschter Unterstützung ganz einfach wechseln können.

Fazit

Wir haben verschiedene Ansätze für den Umgang mit Parallaxe-Websites getestet – von absolut positionierten Elementen bis hin zur Verwendung eines Canvas mit fester Position. Die Implementierung hängt natürlich davon ab, was Sie erreichen möchten und mit dem spezifischen Design, mit dem Sie arbeiten, aber es ist immer gut zu wissen, dass Sie Optionen haben.

Und wie immer: Versuch es nicht, sondern teste es.