Leistungsstarke Parallelisierung

Parallaxing ist nicht mehr wegzudenken. Bei sorgfältiger Verwendung kann es einer Web-App Tiefe und Subtilität verleihen. Das Problem ist jedoch, dass die Implementierung von Parallaxing auf leistungsstarke Weise schwierig sein kann. In diesem Artikel stellen wir eine Lösung vor, die sowohl leistungsstark als auch browserübergreifend ist.

Parallaxen-Illustration

Zusammenfassung

  • Verwenden Sie keine Scroll-Ereignisse oder background-position, um Parallaxe-Animationen zu erstellen.
  • Mit CSS-3D-Transformationen lässt sich ein genauerer Parallaxeneffekt erzielen.
  • Verwenden Sie für Mobile Safari position: sticky, damit der Parallaxeneffekt weitergegeben wird.

Wenn Sie die Drop-in-Lösung verwenden möchten, rufen Sie das GitHub-Repository mit UI-Elementbeispielen auf und laden Sie das Parallax-Helper-JS herunter. Eine Live-Demo des Parallax-Scrollers finden Sie im GitHub-Repository.

Problem-Parallaxen

Sehen wir uns zunächst zwei gängige Methoden an, um einen Parallaxeneffekt zu erzielen, und insbesondere, warum sie für unsere Zwecke ungeeignet sind.

Ungünstig: Scroll-Ereignisse verwenden

Die wichtigste Anforderung für Parallaxing ist, dass es an das Scrollen gekoppelt sein muss. Bei jeder Änderung der Scrollposition der Seite muss sich die Position des Parallaxing-Elements ändern. Das klingt einfach, aber ein wichtiger Mechanismus moderner Browser ist ihre Fähigkeit, asynchron zu arbeiten. In unserem speziellen Fall gilt dies für Scroll-Ereignisse. In den meisten Browsern werden Scroll-Ereignisse auf Best-Effort-Basis ausgeliefert. Es ist nicht garantiert, dass sie bei jedem Frame der Scroll-Animation ausgeliefert werden.

Diese wichtige Information zeigt uns, warum wir eine JavaScript-basierte Lösung vermeiden müssen, bei der Elemente basierend auf Scroll-Ereignissen verschoben werden: JavaScript garantiert nicht, dass die Parallax-Effekte mit der Scrollposition der Seite synchronisiert werden. In älteren Versionen von Mobile Safari wurden Scroll-Ereignisse erst am Ende des Scrollens ausgeliefert, was es unmöglich machte, einen JavaScript-basierten Scrolleffekt zu erstellen. Neuere Versionen liefern Scroll-Ereignisse während der Animation, aber ähnlich wie bei Chrome auf „Best-Effort“-Basis. Wenn der Hauptthread mit anderen Aufgaben beschäftigt ist, werden Scroll-Ereignisse nicht sofort übermittelt. Das bedeutet, dass der Parallaxeneffekt verloren geht.

Schlecht: background-position wird aktualisiert

Eine weitere Situation, die wir vermeiden möchten, ist das Malen auf jedem Frame. Viele Lösungen versuchen, background-position zu ändern, um den Parallaxeneffekt zu erzielen. Dadurch muss der Browser die betroffenen Teile der Seite beim Scrollen neu rendern, was so aufwendig sein kann, dass die Animation deutlich ruckelt.

Wenn wir das Versprechen von Parallax-Bewegungen einhalten möchten, brauchen wir etwas, das als beschleunigte Eigenschaft angewendet werden kann (was heute bedeutet, dass wir uns an Transformationen und Deckkraft halten) und das nicht auf Scroll-Ereignissen basiert.

CSS in 3D

Sowohl Scott Kellum als auch Keith Clark haben sich intensiv mit der Verwendung von CSS 3D für Parallaxenbewegungen beschäftigt. Die von ihnen verwendete Technik ist im Grunde folgende:

  • Richten Sie ein Containerelement ein, das mit overflow-y: scroll (und wahrscheinlich overflow-x: hidden) gescrollt wird.
  • Wenden Sie auf dasselbe Element einen perspective-Wert und einen perspective-origin-Wert an, der auf top left oder 0 0 festgelegt ist.
  • Auf die untergeordneten Elemente dieses Elements wird eine Translation in Z angewendet und sie werden wieder hochskaliert, um einen Parallaxeneffekt zu erzielen, ohne ihre Größe auf dem Bildschirm zu beeinflussen.

Das CSS für diesen Ansatz sieht so aus:

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

Dabei wird von einem HTML-Snippet wie diesem ausgegangen:

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

Maßstab für die Perspektive anpassen

Wenn Sie das untergeordnete Element nach hinten verschieben, wird es proportional zum Perspektivenwert kleiner. Mit dieser Gleichung können Sie berechnen, um wie viel die Perspektive vergrößert werden muss: (Perspektive – Entfernung) ÷ Perspektive. Da wir höchstwahrscheinlich möchten, dass das Parallax-Element den Parallax-Effekt aufweist, aber in der von uns festgelegten Größe angezeigt wird, muss es auf diese Weise skaliert werden, anstatt unverändert zu bleiben.

Im obigen Codebeispiel ist die Perspektive 1px und die Z-Entfernung des parallax-child-Elements -2px. Das Element muss also um den Faktor 3 vergrößert werden. Das ist der Wert, der im Code angegeben ist: scale(3).

Für Inhalte, auf die kein translateZ-Wert angewendet wurde, können Sie den Wert „0“ einsetzen. Das bedeutet, dass der Skalierungsfaktor (Perspektive – 0) / Perspektive ist, was einen Wert von 1 ergibt. Der Wert wurde also weder nach oben noch nach unten skaliert. Sehr praktisch.

Funktionsweise dieses Ansatzes

Es ist wichtig, dass Sie verstehen, warum das funktioniert, da wir dieses Wissen gleich nutzen werden. Das Scrollen ist im Grunde eine Transformation und kann daher beschleunigt werden. Dabei werden die Ebenen hauptsächlich mit der GPU verschoben. Bei einem typischen Scrollen, bei dem es keine Perspektive gibt, erfolgt das Scrollen im Verhältnis 1:1, wenn das scrollelement und seine untergeordneten Elemente verglichen werden. Wenn Sie ein Element um 300px nach unten scrollen, werden seine untergeordneten Elemente um denselben Betrag nach oben transformiert: 300px.

Wenn Sie dem Scrolling-Element jedoch einen Perspektivwert zuweisen, wird dieser Prozess beeinträchtigt, da sich die Matrizen ändern, die der Scroll-Transformation zugrunde liegen. Wenn Sie beispielsweise perspective auf 0,5 und translateZ auf 0,5 festlegen, werden die untergeordneten Elemente bei einem Scrollen um 300 Pixel nur um 150 Pixel verschoben. Wenn ein Element einen translateZ-Wert von 0 hat, wird es im Verhältnis 1:1 gescrollt (wie bisher). Ein untergeordnetes Element, das in Z vom Perspektivenursprung weg verschoben wird, wird jedoch mit einer anderen Geschwindigkeit gescrollt. Nettoergebnis: Parallaxenbewegung. Und das ist sehr wichtig: Das Scrollen wird automatisch von den internen Scrollfunktionen des Browsers übernommen. Sie müssen also nicht auf scroll-Ereignisse warten oder background-position ändern.

Ein Wermutstropfen: Mobile Safari

Für jeden Effekt gibt es Einschränkungen. Eine wichtige Einschränkung für Transformationen betrifft die Beibehaltung von 3D-Effekten für untergeordnete Elemente. Wenn sich in der Hierarchie zwischen dem Element mit einer Perspektive und den Parallax-untergeordneten Elementen weitere Elemente befinden, wird die 3D-Perspektive „abgeflacht“, d. h., der Effekt geht verloren.

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

Im obigen HTML-Code ist .parallax-container neu. Dadurch wird der Wert perspective effektiv reduziert und der Parallaxeneffekt geht verloren. Die Lösung ist in den meisten Fällen recht einfach: Sie fügen dem Element transform-style: preserve-3d hinzu, wodurch alle 3D-Effekte (z. B. unser Perspektivenwert) übernommen werden, die weiter oben im Baum angewendet wurden.

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

Bei Mobile Safari ist es jedoch etwas komplizierter. Die Anwendung von overflow-y: scroll auf das Containerelement funktioniert zwar technisch, aber das Scrollen des Elements ist dann nicht mehr möglich. Die Lösung besteht darin, -webkit-overflow-scrolling: touch hinzuzufügen. Dadurch wird aber auch perspective reduziert und es gibt keinen Parallaxeneffekt.

Aus Sicht von Progressive Enhancement ist das wahrscheinlich kein großes Problem. Wenn wir nicht in jeder Situation einen Parallax-Effekt erzielen können, funktioniert unsere App trotzdem. Es wäre aber schön, eine Lösung zu finden.

position: sticky eilt zu Hilfe!

Tatsächlich gibt es eine Hilfestellung in Form von position: sticky, mit der Elemente beim Scrollen oben im Viewport oder in einem bestimmten übergeordneten Element „fixiert“ werden können. Die Spezifikation ist wie die meisten ziemlich umfangreich, enthält aber ein hilfreiches kleines Juwel:

Das mag auf den ersten Blick nicht viel bedeuten, aber ein wichtiger Punkt in diesem Satz ist, wie genau die Stickiness eines Elements berechnet wird: „Der Offset wird in Bezug auf das nächste übergeordnete Element mit einem Scrollfeld berechnet.“. Mit anderen Worten: Die Distanz, um das fixierte Element zu verschieben (damit es an einem anderen Element oder dem Anzeigebereich angehängt wird), wird vor allen anderen Transformationen berechnet, nicht danach. Das bedeutet, dass sich der Offset, ähnlich wie im vorherigen Beispiel mit dem Scrollen, bei 300 px berechnen lässt. Sie können Perspektiven (oder eine andere Transformation) verwenden, um diesen Offsetwert von 300 px zu bearbeiten, bevor er auf fixierte Elemente angewendet wird.

Durch Anwenden von position: -webkit-sticky auf das Parallaxing-Element können wir den Abflachungseffekt von -webkit-overflow-scrolling: touch effektiv „umkehren“. So wird sichergestellt, dass das Parallaxing-Element auf den nächsten übergeordneten Knoten mit einem Scrollfeld verweist, in diesem Fall .container. Dann wird wie zuvor mit .parallax-container ein perspective-Wert angewendet, der den berechneten Scroll-Offset ändert und einen Parallaxeneffekt erzeugt.

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

Dadurch wird der Parallaxeneffekt für Mobile Safari wiederhergestellt. Das ist eine gute Nachricht für alle!

Hinweise zur fixierten Positionierung

Es gibt hier jedoch einen Unterschied: position: sticky ändert die Parallaxenmechanik. Bei der Sticky-Positionierung wird versucht, das Element an den Scrolling-Container zu „kleben“, während dies bei einer nicht-sticky-Version nicht der Fall ist. Das bedeutet, dass der Parallaxeneffekt mit fixierten Enden das Gegenteil des Effekts ohne fixierte Enden ist:

  • Mit position: sticky bewegt sich das Element weniger, je näher es sich an z=0 befindet.
  • Ohne position: sticky bewegt sich das Element stärker, je näher es an z=0 ist.

Wenn das alles etwas abstrakt erscheint, sehen Sie sich diese Demo von Robert Flack an. Darin wird gezeigt, wie sich Elemente mit und ohne „sticky“-Positionierung unterschiedlich verhalten. Um den Unterschied zu sehen, benötigen Sie Chrome Canary (derzeit Version 56) oder Safari.

Screenshot mit Parallaxenperspektive

Demo von Robert Flack, in der gezeigt wird, wie sich position: sticky auf das Parallax-Scrolling auswirkt.

Verschiedene Fehler und Workarounds

Wie bei allem gibt es jedoch noch einige Unebenheiten, die beseitigt werden müssen:

  • Die Unterstützung für das Anbringen von Notizen ist nicht einheitlich. Die Unterstützung wird noch in Chrome implementiert, Edge bietet überhaupt keine Unterstützung und in Firefox gibt es Darstellungsfehler, wenn „sticky“ mit Perspektiventransformationen kombiniert wird. In solchen Fällen ist es sinnvoll, etwas Code hinzuzufügen, um position: sticky (die Version mit dem Präfix -webkit-) nur bei Bedarf hinzuzufügen, also nur für Mobile Safari.
  • Der Effekt funktioniert nicht einfach so in Edge. Edge versucht, das Scrollen auf Betriebssystemebene zu verarbeiten. Das ist im Allgemeinen gut, verhindert in diesem Fall jedoch, dass die Perspektivänderungen während des Scrollens erkannt werden. Um dieses Problem zu beheben, können Sie ein Element mit fester Position hinzufügen. Dadurch wird Edge anscheinend auf eine Scrollmethode ohne Betriebssystem umgestellt, die Perspektivänderungen berücksichtigt.
  • „Der Inhalt der Seite ist einfach riesig!“ Viele Browser berücksichtigen die Skalierung bei der Entscheidung, wie groß der Inhalt der Seite ist. Chrome und Safari berücksichtigen die Perspektive jedoch nicht. Wenn also beispielsweise ein Element mit dem Faktor 3 skaliert wird, werden möglicherweise Scrollleisten usw. angezeigt, auch wenn das Element nach Anwendung von perspective mit dem Faktor 1 skaliert wird. Sie können dieses Problem umgehen, indem Sie Elemente über die untere rechte Ecke (mit transform-origin: bottom right) skalieren. Das funktioniert, weil übergroße Elemente dadurch in den „negativen Bereich“ (in der Regel oben links) des scrollbaren Bereichs verschoben werden. In scrollbaren Bereichen können Sie Inhalte im negativen Bereich nie sehen oder dorthin scrollen.

Fazit

Parallaxe ist ein interessanter Effekt, wenn er durchdacht eingesetzt wird. Wie Sie sehen, ist es möglich, die Funktion leistungsstark, scrollgebunden und browserübergreifend zu implementieren. Da dafür etwas mathematisches Geschick und eine kleine Menge an Boilerplate-Code erforderlich sind, haben wir eine kleine Hilfsbibliothek und ein Beispiel erstellt, die Sie in unserem GitHub-Repository für UI-Elementbeispiele finden.

Probieren Sie es aus und teilen Sie uns mit, wie es Ihnen gefällt.