Verhindern von Inhaltsverschiebungen durch Lazy-Loading-Bilder

Avatar of James Steinbach
James Steinbach am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Sie kennen das Konzept des Lazy Loadings für Bilder. Es verhindert, dass der Browser Bilder lädt, bis diese Bilder im (oder fast im) Ansichtsfenster des Browsers erscheinen.

Es gibt eine Fülle von JavaScript-basierten Lazy-Loading-Lösungen. GitHub hat über 3.400 verschiedene Lazy-Load-Repos, und das sind nur die, bei denen "lazy load" in einer durchsuchbaren Zeichenkette vorkommt! Die meisten davon basieren auf demselben Trick: Anstatt die URL eines Bildes im src-Attribut zu platzieren, platziert man sie in data-src – was dasselbe Muster für responsive Bilder ist.

  • JavaScript beobachtet, wie der Benutzer die Seite nach unten scrollt.
  • Wenn der Benutzer auf ein Bild stößt, verschiebt JavaScript den Wert von data-src in src, wo er hingehört.
  • Der Browser fordert das Bild an und es wird angezeigt.

Das Ergebnis ist, dass der Browser weniger Bilder im Voraus lädt, sodass die Seite schneller geladen wird. Darüber hinaus, wenn der Benutzer nie weit genug scrollt, um ein Bild zu sehen, wird dieses Bild nie geladen. Das bedeutet schnellere Seitenladezeiten und weniger Daten, die der Benutzer ausgeben muss.

„Das ist erstaunlich!“ denken Sie vielleicht. Und Sie haben Recht… es ist erstaunlich!

Das gesagt, es führt tatsächlich zu einem bemerkenswerten Problem: Bilder, die das src-Attribut nicht enthalten (auch wenn es leer oder ungültig ist), haben keine Höhe. Das bedeutet, dass sie bis zum Lazy-Loading nicht die richtige Größe im Seitenlayout haben.

Update! Die Zeiten haben sich hier schnell geändert, und um Lazy-Load-Ruckler zu vermeiden, müssen Sie lediglich die korrekten nativen Breiten- und Höhenattribute für Bilder angeben, und sie werden schön geladen, auch wenn CSS das Bild flüssig macht. Machen Sie es also so: <img src="bild.jpg" width="800" height="600">

Wenn ein Benutzer scrollt und Bilder per Lazy-Loading geladen werden, gehen diese img-Elemente von einer Höhe von 0 Pixeln auf die benötigte Höhe über. Dies verursacht **Reflow**, bei dem der Inhalt unter oder um das Bild herum verschoben wird, um Platz für das frisch geladene Bild zu schaffen. Reflow ist ein Problem, da es sich um eine benutzerblockierende Operation handelt. Es verlangsamt den Browser, indem es ihn zwingt, das Layout aller Elemente neu zu berechnen, die von der Form dieses Bildes betroffen sind. Die CSS-Eigenschaft scroll-behavior könnte hier irgendwann helfen, aber ihre Unterstützung muss sich verbessern, bevor sie eine praktikable Option ist.

Lazy Loading garantiert nicht, dass das Bild vollständig geladen wird, bevor es in den Viewport gelangt. Das Ergebnis ist eine gefühlt ruckelige Erfahrung, auch wenn es ein großer Leistungsgewinn ist.

Es gibt weitere Probleme mit dem Lazy Loading von Bildern, die erwähnenswert sind, aber außerhalb des Rahmens dieses Beitrags liegen. Wenn beispielsweise JavaScript überhaupt nicht ausgeführt wird, werden keine Bilder auf der Seite geladen. Das ist eine häufige Sorge für jede JavaScript-basierte Lösung, aber dieser Artikel befasst sich nur mit der Lösung der Probleme, die durch Reflow entstehen.

Wenn wir vorab geladene Bilder zwingen könnten, ihre normale Breite und Höhe (d. h. ihr Seitenverhältnis) beizubehalten, könnten wir Reflow-Probleme vermeiden und sie trotzdem per Lazy-Loading laden. Dies ist etwas, das ich kürzlich beim Erstellen einer Progressive Web App bei DockYard, wo ich arbeite, lösen musste.

Für zukünftige Referenz gibt es ein HTML-Attribut namens intrinsicsize, das dazu dient, das Seitenverhältnis beizubehalten, aber derzeit ist dies in Chrome nur experimentell.

Hier ist, wie wir es gemacht haben.

Seitenverhältnis beibehalten

Es gibt viele Möglichkeiten, wie wir Seitenverhältnisse beibehalten können. Chris hat einmal eine erschöpfende Liste von Optionen zusammengestellt, aber hier sind die bildspezifischen Optionen, die wir uns ansehen.

Das Bild selbst

Der Bild-src liefert ein natürliches Seitenverhältnis. Selbst wenn ein Bild responsiv skaliert wird, gelten seine natürlichen Abmessungen immer noch. Hier ist ein ziemlich gängiger Teil von responsivem Bild-CSS

img {
  max-width: 100%;
  height: auto;
}

Dieses CSS besagt, dass Bilder die Breite des Elements, das sie enthält, nicht überschreiten sollen, aber die Höhe richtig skalieren, sodass es kein „Dehnen“ oder „Stauchen“ gibt, wenn das Bild skaliert wird. Selbst wenn das Bild Inline-height- und width-Attribute hat, werden sie diese auf kleinen Viewports weiterhin schön verhalten lassen.

Dieses Verhalten des „natürlichen Seitenverhältnisses“ bricht jedoch zusammen, wenn noch kein src vorhanden ist. Browser kümmern sich nicht um data-src und tun nichts damit, sodass es keine wirklich praktikable Lösung für Lazy-Loading-Reflow ist, aber es ist wichtig, um die „normale“ Art und Weise zu verstehen, wie Bilder einmal geladen werden.

Ein Pseudo-Element

Viele Entwickler – mich eingeschlossen – waren frustriert, als sie versuchten, Pseudo-Elemente (z. B. ::before und ::after) zu verwenden, um img-Elementen Dekorationen hinzuzufügen. Browser rendern keine Pseudo-Elemente von Bildern, da img ein ersetztes Element ist, was bedeutet, dass sein Layout von einer externen Ressource gesteuert wird.

Es gibt jedoch eine Ausnahme von dieser Regel: Wenn das src-Attribut eines Bildes ungültig ist, rendern Browser seine Pseudo-Elemente. Wenn wir also das src für ein Bild in data-src speichern und das src leer ist, können wir ein Pseudo-Element verwenden, um ein Seitenverhältnis festzulegen.

[data-src]::before {
  content: '';
  display: block;
  padding-top: 56.25%;
}

Das setzt ein Seitenverhältnis von 16:9 auf ::before für jedes Element mit einem data-src-Attribut. Sobald data-src zum src wird, stoppt der Browser das Rendern von ::before und das natürliche Seitenverhältnis des Bildes übernimmt.

Hier ist eine Demo

Siehe den Pen Image Aspect Ratio: ::before padding von James Steinbach (@jdsteinbach) auf CodePen.

Diese Lösung hat jedoch ein paar Nachteile. Erstens beruht sie auf der Zusammenarbeit von CSS und HTML. Ihre Stylesheet muss eine Deklaration für jedes Bildseitenverhältnis enthalten, das Sie unterstützen müssen. Es wäre viel besser, wenn die Vorlage ein Bild einfügen könnte, *ohne* CSS-Änderungen zu benötigen.

Zweitens funktioniert es zum Zeitpunkt der Erstellung nicht in Safari 12 und darunter oder Edge. Das ist ein ziemlich großer Verkehrsanteil, um schlechte Layouts zu verschicken. Um fair zu sein, die Aufrechterhaltung des Seitenverhältnisses ist eine Art progressive Verbesserung – an der endgültig gerenderten Seite ist nichts „kaputt“. Dennoch ist es viel besser, das Reflow-Problem zu lösen und dass Bilder wie erwartet gerendert werden.

Data URI (Base64) PNGs

Ein weiterer Versuch, das Seitenverhältnis zu erhalten, war das Einbetten von Data URIs für den src als PNG. Mit png-pixel.com wird die gesamte Base64-Codierung mit beliebigen Dimensionen und Farben erleichtert. Dies kann direkt in das src-Attribut des Bildes im HTML eingefügt werden.

<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAQAAAA3fa6RAAAADklEQVR42mNkAANGCAUAACMAA2w/AMgAAAAASUVORK5CYII=" data-src="//picsum.photos/900/600" alt="Lazy loading test image" />

Das Inline-PNG hat ein Seitenverhältnis von 3:2 (dasselbe Seitenverhältnis wie das endgültige Bild). Wenn src durch den Wert von data-src ersetzt wird, behält das Bild sein Seitenverhältnis genau so bei, wie wir es wollen!

Hier ist eine Demo

Siehe den Pen Image Aspect Ratio: inline base64 PNG von James Steinbach (@jdsteinbach) auf CodePen.

Und ja, dieser Ansatz hat auch einige Nachteile. Obwohl die Browserunterstützung viel besser ist, ist sie kompliziert zu warten. Wir müssen eine Base64-Zeichenkette für jede neue Bildgröße generieren und dieses Objekt von Zeichenketten dann für jedes verwendete Templating-Tool verfügbar machen. Es ist auch nicht die effizienteste Methode, diese Daten darzustellen.

Ich habe weiter erforscht und einen kleineren Weg gefunden.

SVG mit Base64 kombinieren

Nachdem ich die Inline-PNG-Option untersucht hatte, fragte ich mich, ob SVG ein kleineres Format für Inline-Bilder sein könnte, und hier ist, was ich herausgefunden habe: Ein SVG mit einer viewBox-Deklaration ist ein Platzhalterbild mit einem leicht editierbaren nativen Seitenverhältnis.

Zuerst habe ich versucht, ein SVG Base64-zu-codieren. Hier ist ein Beispiel dafür, wie das in meinem HTML aussah.

<img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHZpZXdCb3g9JzAgMCAzIDInPjwvc3ZnPg==" data-src="//picsum.photos/900/600" alt="Lazy loading test image">

Bei kleinen, einfachen Seitenverhältnissen ist dies ungefähr gleich groß wie die Base64-PNGs. Ein Verhältnis von 1:1 wäre 114 Bytes mit Base64-PNG und 106 Bytes mit Base64-SVG. Ein Verhältnis von 2:3 beträgt 118 Bytes mit Base64-PNG und 106 Bytes mit Base64-SVG.

Bei größeren, komplexeren Verhältnissen bleiben die Base64-SVGs jedoch klein, was ein echter Vorteil bei der Dateigröße ist. Ein Verhältnis von 16:9 beträgt 122 Bytes in Base64-PNG und 110 Bytes in Base64-SVG. Ein Verhältnis von 923:742 beträgt 3.100 Bytes in Base64-PNG, aber nur 114 Bytes in Base64-SVG! (Das ist kein gängiges Seitenverhältnis, aber ich musste es für den Anwendungsfall meines Kunden mit benutzerdefinierten Abmessungen testen.)

Hier ist eine Tabelle, um diese Vergleiche klarer zu sehen.

Seitenverhältnis base64 PNG base64 SVG
1:1 114 Bytes 106 Bytes
2:3 118 Bytes 106 Bytes
16:9 122 Bytes 110 Bytes
923:742 3.100 Bytes 114 Bytes

Die Unterschiede sind bei einfachen Verhältnissen vernachlässigbar, aber Sie sehen, wie gut sich SVG skaliert, wenn die Verhältnisse komplexer werden.

Wir haben jetzt eine viel bessere Browserunterstützung. Diese Technik wird von allen großen Anbietern unterstützt, einschließlich Chrome, Firefox, Safari, Opera, IE11 und Edge, hat aber auch großartige Unterstützung in mobilen Browsern, einschließlich Safari iOS, Chrome für Android und Samsung für Android (ab Version 4.4).

Hier ist eine Demo

Siehe den Pen Image Aspect Ratio: inline base64 SVG von James Steinbach (@jdsteinbach) auf CodePen.

🏆 Wir haben einen Gewinner!

Ja, das haben wir, aber bleiben Sie bei mir, während wir diesen Ansatz noch weiter verbessern! Ich erinnerte mich daran, dass Chris vorschlug, dass wir keine Base64-Codierung mit in CSS-Hintergrundbildern eingebetteten SVGs verwenden sollten und dachte, dieser Rat könnte auch hier zutreffen.

In diesem Fall habe ich anstelle der Base64-Codierung der SVGs die „Optimized URL-encoded“-Technik aus diesem Beitrag verwendet. Hier ist die Markierung.

<img src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 3 2'%3E%3C/svg%3E" data-src="//picsum.photos/900/600" alt="Lazy loading test image" />

Das ist nur geringfügig kleiner als Base64-SVG. Das 1:1 ergibt 106 Bytes in Base64 und 92 Bytes bei URL-Codierung. 16:9 ergibt 110 Bytes in Base64 und 97 Bytes bei URL-Codierung.

Wenn Sie mehr über die Datengröße nach Datei und Codierungsformat erfahren möchten, vergleicht diese Demo verschiedene Byte-Größen zwischen all diesen Techniken.

Die *wirklichen* Vorteile, die das URL-codierte SVG zu einem klaren Gewinner machen, sind jedoch, dass sein Format menschenlesbar, einfach zu templaten und unendlich anpassbar ist!

Sie müssen keinen CSS-Block erstellen oder eine Base64-Zeichenkette generieren, um einen perfekten Platzhalter für Bilder zu erhalten, deren Abmessungen unbekannt sind! Hier ist zum Beispiel eine kleine React-Komponente, die diese Technik verwendet.

const placeholderSrc = (width, height) => `data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"%3E%3C/svg%3E`

const lazyImage = ({url, width, height, alt}) => {
  return (
    <img
      src={placeholderSrc(width, height)}
      data-src={url}
      alt={alt} />
  )
}

Siehe den Pen React LazyLoad Image with Stable Aspect Ratio von James Steinbach (@jdsteinbach) auf CodePen.

Oder, wenn Sie Vue bevorzugen.

Siehe den Pen Vue LazyLoad Image with Stable Aspect Ratio von James Steinbach (@jdsteinbach) auf CodePen.

Ich freue mich, berichten zu können, dass sich die Browserunterstützung mit dieser Verbesserung nicht geändert hat – wir haben immer noch die volle Unterstützung wie bei Base64-SVG!

Fazit

Wir haben mehrere Techniken untersucht, um Inhaltsverschiebungen zu verhindern, indem wir das Seitenverhältnis eines per Lazy-Loading geladenen Bildes beibehalten, bevor der Austausch stattfindet. Die beste Technik, die ich finden konnte, ist ein eingebettetes und optimiertes URL-codiertes SVG mit in der viewBox-Attribute definierten Bildabmessungen. Dies kann mit einer Funktion wie dieser geskriptet werden.

const placeholderSrc = (width, height) => `data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}"%3E%3C/svg%3E`

Es gibt mehrere Vorteile dieser Technik.

  • Solide Browserunterstützung auf Desktop und Mobilgeräten.
  • Kleinste Byte-Größe.
  • Menschenlesbares Format.
  • Einfach zu templaten ohne Laufzeit-Codierungsaufrufe.
  • Unendlich erweiterbar.

Was halten Sie von diesem Ansatz? Haben Sie etwas Ähnliches verwendet oder haben Sie eine völlig andere Methode zur Behandlung von Reflow? Lassen Sie es mich wissen!