Tipps zum Erstellen des eigenen Lazy Loading

Avatar of Phil Hawksworth
Phil Hawksworth am

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

Sie haben vielleicht gehört (oder sogar den Ruf erlassen), dass „wir einfach Lazy Loading verwenden können!“, wenn wir nach einer Möglichkeit suchen, eine besonders schwere Webseite zu optimieren.

Lazy Loading ist eine gängige Technik, um Bilder schrittweise anzufordern, wenn sie in den sichtbaren Bereich gelangen, anstatt alle auf einmal nach dem Parsen des Seiten-HTMLs. Dies kann das anfängliche Seitenvolumen reduzieren und uns helfen, unsere Performance-Budgets einzuhalten, indem Bilder nur bei Bedarf angefordert werden.

Es kann effektiv sein. Aber es bringt auch einige Nachteile mit sich. Dazu kommen wir noch! Tatsächlich hat Rahul Nanwani eine ausführliche Abhandlung über mehrere Lazy-Loading-Methoden verfasst, die zeigt, wie komplex einige davon sind.

In diesem Beitrag werden wir eine Implementierung betrachten, die bereits in diesem Beitrag von Preethi kurz behandelt wurde. Wir werden diese erweitern, damit Sie Ihre eigene Lazy-Loading-Implementierung auf Ihrer Website hinzufügen können, so wie ich es auf dieser kleinen Demo-Website getan habe.

Wir werden auf folgenden Themen eingehen:

Das werden wir bauen

Warum nicht natives Lazy Loading?

Derzeit ist Lazy Loading keine Funktion, die Browser nativ für uns übernehmen können. Obwohl sich dies in einigen Browsern bald ändern wird, mit der Einführung von Chrome 75, das Lazy Loading für Bilder und iframes unterstützen soll. Bis dahin (und auch danach, wenn wir uns gut mit anderen Browsern verstehen wollen – was wir sollten) wird Lazy Loading mithilfe von JavaScript implementiert. Es gibt eine Reihe von Bibliotheken und Frameworks, die dabei helfen.

Einige Static Site Generatoren, Bibliotheken und Frameworks enthalten Dienstprogramme, die diese Funktion "out of the box" bereitstellen, was sich als beliebt erweist, da viele nach integrierten Möglichkeiten suchen, diese Funktion in ihre Websites einzubauen. Ich habe jedoch auch einen Trend bemerkt, bei dem einige ganze Bibliotheken oder Frameworks übernehmen, nur um Zugang zu dieser Funktion zu erhalten. Als sparsamer, auf Performance und Inklusivität bedachter Spaßvogel bin ich dem gegenüber etwas skeptisch. Lassen Sie uns also betrachten, wie Sie dies selbst implementieren könnten, ohne auf ein bestimmtes Framework oder eine Bibliothek angewiesen zu sein.

lazy loading example

Die typischen Abläufe für Lazy Loading

Die meisten Ansätze folgen einem Muster wie diesem:

Zuerst etwas HTML, um unsere Lazy-Loading-Bilder zu definieren.

<!-- 
Don't include a src attribute in images you wish to load lazily.
Instead specify their src safely in a data attribute
-->
<img data-src="lighthouse.jpg" alt="A snazzy lighthouse" class="lazy" />

Wann sollten Bilder geladen werden?

Als Nächstes verwenden wir eine Art JavaScript-Magie, um das src-Attribut korrekt zu setzen, wenn das Bild in den sichtbaren Bereich gelangt. Dies war einst eine aufwendige JavaScript-Operation, die das Lauschen auf Scroll- und Resize-Events des Fensters beinhaltete, aber IntersectionObserver hat Abhilfe geschaffen.

Ein Intersection Observer wird so erstellt:

// Set up an intersection observer with some options
var observer = new IntersectionObserver(lazyLoad, {

  // where in relation to the edge of the viewport, we are observing
  rootMargin: "100px",

  // how much of the element needs to have intersected 
  // in order to fire our loading function
  threshold: 1.0

});

Wir haben unseren neuen Observer angewiesen, eine Funktion namens lazyLoad aufzurufen, wenn die beobachteten Bedingungen erfüllt sind. Die Elemente, die diese Bedingungen erfüllen, werden an diese Funktion übergeben, damit wir sie manipulieren können... zum Beispiel, um sie tatsächlich zu laden und anzuzeigen.

function lazyLoad(elements) {
  elements.forEach(image => {
    if (image.intersectionRatio > 0) {

      // set the src attribute to trigger a load
      image.src = image.dataset.src;

      // stop observing this element. Our work here is done!
      observer.unobserve(item.target);
    };
  });
};

Gut. Unsere Bilder erhalten das korrekte src-Attribut zugewiesen, wenn sie in den sichtbaren Bereich gelangen, was sie lädt. Aber welche Bilder? Wir müssen der Intersection Observer API mitteilen, welche Elemente uns interessieren. Glücklicherweise haben wir jedem Element eine CSS-Klasse von .lazy zugewiesen, genau zu diesem Zweck.

// Tell our observer to observe all img elements with a "lazy" class
var lazyImages = document.querySelectorAll('img.lazy');
lazyImages.forEach(img => {
  observer.observe(img);
});

Schön, aber perfekt?

Das scheint gut zu funktionieren, aber es gibt einige Nachteile zu beachten:

  1. Bis (oder es sei denn) JavaScript geladen wird und erfolgreich ausgeführt wird, haben wir eine Menge Bildelemente auf unserer Seite, die nicht funktionieren werden. Wir haben sie bewusst unbrauchbar gemacht, indem wir das src-Attribut entfernt haben. Das ist das Ergebnis, das wir uns gewünscht haben, aber jetzt sind wir von JavaScript abhängig, damit diese Bilder geladen werden. Während es stimmt, dass JavaScript heutzutage im Web allgegenwärtig ist – das Web erreicht ein breites Spektrum an Geräten und Netzwerbedingungen –, kann JavaScript eine teure Ergänzung zu unseren Performance-Budgets werden, insbesondere wenn es an der Auslieferung und Darstellung von Inhalten beteiligt ist. Wie Jake Archibald einst bemerkte: alle Ihre Benutzer sind Nicht-JS, während sie Ihr JS herunterladen. Anders ausgedrückt, dies ist nichts, was man leichtfertig nehmen sollte.
  2. Selbst wenn dies erfolgreich funktioniert, haben wir leere Elemente auf unserer Seite, die beim Laden vielleicht einen kleinen visuellen Ruck auslösen. Vielleicht können wir das Bild zuerst andeuten und etwas Besonderes tun. Dazu kommen wir gleich.

Die geplante native Lazy-Loading-Implementierung von Chrome sollte bei unserem ersten Punkt helfen. Wenn dem Element ein loading-Attribut gegeben wurde, kann Chrome das src-Attribut zur richtigen Zeit berücksichtigen, anstatt es sofort anzufordern, sobald es im HTML erscheint.

Der Editor-Entwurf der Spezifikation enthält Unterstützung für verschiedene Ladeverhalten:

  • <img loading="lazy" />: Sagt dem Browser, dass dieses Bild bei Bedarf lazy geladen werden soll.
  • <img loading="eager" />: Sagt dem Browser, dass dieses Bild sofort geladen werden soll.
  • <img loading="auto" />: Lässt den Browser seine eigene Einschätzung vornehmen.

Browser ohne diese Unterstützung könnten das Bild dank der resilienten Natur von HTML und Browsern, die unbekannte HTML-Attribute ignorieren, normal laden.

Aber… schlagen Sie Alarm! Dieses Feature ist noch nicht in Chrome angekommen, und es gibt auch Unsicherheit, ob und wann andere Browser es implementieren werden. Wir können Feature Detection verwenden, um zu entscheiden, welche Methode wir verwenden, aber dies bietet immer noch keinen soliden Ansatz für progressive Verbesserung, bei dem die Bilder keine Abhängigkeit von JavaScript haben.

<img data-src="lighthouse.jpg" alt="A snazzy lighthouse" loading="lazy" class="lazy" />
// If the browser supports lazy loading, we can safely assign the src
// attributes without instantly triggering an eager image load.
if ("loading" in HTMLImageElement.prototype) {
  const lazyImages = document.querySelectorAll("img.lazy");
  lazyImages.forEach(img => {
    img.src = img.dataset.src;
  });
}
else {
  // Use our own lazyLoading with Intersection Observers and all that jazz
}

Als Begleiter zu responsiven Bildern

Unter der Annahme, dass wir mit der Tatsache einverstanden sind, dass JavaScript vorerst eine Abhängigkeit darstellt, wenden wir uns einem verwandten Thema zu: responsive Bilder.

Wenn wir schon den Aufwand betreiben, Bilder erst bei Bedarf in den Browser zu liefern, scheint es fair, dass wir auch sicherstellen wollen, dass wir sie in der besten Größe liefern, je nachdem, wie sie angezeigt werden. Zum Beispiel gibt es keinen Grund, die 1200px breite Version eines Bildes herunterzuladen, wenn das Gerät, das es anzeigt, ihm nur eine Breite von 400px gibt. Optimieren wir!

HTML bietet uns einige Möglichkeiten, responsive Bilder zu implementieren, die unterschiedliche Bildquellen mit unterschiedlichen Viewport-Bedingungen verknüpfen. Ich verwende gerne das picture-Element, wie hier:

<picture>
  <source srcset="massive-lighthouse.jpg" media="(min-width: 1200px)">
  <source srcset="medium-lighthouse.jpg" media="(min-width: 700px)">
  <source srcset="small-lighthouse.jpg" media="(min-width: 300px)">
  <img src="regular-lighthouse.jpg" alt="snazzy lighthouse" />
</picture>

Sie werden bemerken, dass jedes source-Element ein srcset-Attribut hat, das eine Bild-URL angibt, und ein media-Attribut, das die Bedingungen definiert, unter denen diese Quelle verwendet werden soll. Der Browser wählt die am besten geeignete Quelle aus der Liste entsprechend den Medienbedingungen, wobei ein Standard-img-Element als Standard/Fallback dient.

Können wir diese beiden Ansätze kombinieren, um responsive Bilder mit Lazy Loading zu erstellen?

Natürlich können wir das! Machen wir es.

Anstatt ein leeres Bild zu haben, bis wir unser Lazy Loading durchführen, lade ich gerne ein Platzhalterbild mit winziger Dateigröße. Dies verursacht zwar den Overhead von mehr HTTP-Anfragen, gibt aber auch einen schönen Effekt, indem es das Bild andeutet, bevor es ankommt. Sie haben diesen Effekt vielleicht schon bei Medium gesehen oder als Ergebnis einer Seite, die das Lazy Loading von Gatsby verwendet.

Wir können dies erreichen, indem wir zunächst die Bildquellen in unserem picture-Element als winzige Versionen desselben Assets definieren und dann CSS verwenden, um sie auf die gleiche Größe wie ihre höher aufgelösten Geschwister zu skalieren. Dann können wir über unseren Intersection Observer jede der angegebenen Quellen aktualisieren, um auf die richtigen Bildquellen zu verweisen.

Anfangs könnte unser picture-Element also so aussehen:

<picture class="lazy">
  <source srcset="tiny-lighthouse.jpg" media="(min-width: 1200px)">
  <source srcset="tiny-lighthouse.jpg" media="(min-width: 700px)">
  <source srcset="tiny-lighthouse.jpg" media="(min-width: 300px)">
  <img src="tiny-lighthouse.jpg" alt="snazzy cake" />
</picture>

Unabhängig von der Viewport-Größe wird ein winziges 20px Bild angezeigt. Wir werden es im nächsten Schritt mit CSS aufblasen.

Vorschau des Bildes mit Stil

Der Browser kann das winzige Vorschau-Bild mit CSS hochskalieren, damit es in das gesamte picture-Element passt, anstatt nur auf 20px. Wie Sie sich vorstellen können, wird es bei der Hochskalierung eines niedrig aufgelösten Bildes auf größere Abmessungen etwas... pixelig.

picture {
  width: 100%; /* stretch to fit its containing element */
  overflow: hidden;
}

picture img {
  width: 100%; /* stretch to fill the picture element */
}
blurred image

Um die durch die Skalierung des Bildes entstehende Pixeligkeit zu mildern, können wir einen Weichzeichnerfilter verwenden.

picture.lazy img {
  filter: blur(20px);
}
more fully blurred image

Quellen mit JavaScript wechseln

Mit einer kleinen Anpassung können wir die gleiche Technik wie zuvor verwenden, um die korrekten URLs für unsere srcset- und src-Attribute zu setzen.

function lazyLoad(elements) {

  elements.forEach(picture => {
    if (picture.intersectionRatio > 0) {

      // gather all the image and source elements in this picture
      var sources = picture.children;

      for (var s = 0; s < sources.length; s++) {
        var source = sources[s];

        // set a new srcset on the source elements 
        if (sources.hasAttribute("srcset")) {
          source.setAttribute("srcset", ONE_OF_OUR_BIGGER_IMAGES);
        }
        // or a new src on the img element
        else {
          source.setAttribute("src", ONE_OF_OUR_BIGGER_IMAGES);
        }
      }

      // stop observing this element. Our work here is done!
      observer.unobserve(item.target);
    };
  });

};

Ein letzter Schritt, um den Effekt zu vervollständigen: Entfernen Sie den Weichzeichner-Effekt aus dem Bild, sobald die neue Quelle geladen wurde. Ein JavaScript-Event-Listener, der auf das load-Ereignis jeder neuen Bildressource wartet, kann dies für uns erledigen.

// remove the lazy class when the full image is loaded to unblur
source.addEventListener('load', image => {
  image.target.closest("picture").classList.remove("lazy")
}, false);

Mit einem Hauch von CSS können wir einen schönen Übergang erstellen, der den Weichzeichner sanft ausblendet.

picture img {
  ...
  transition: filter 0.5s,
}

Ein kleiner Helfer von unseren Freunden

Großartig. Mit ein wenig JavaScript, ein paar Zeilen CSS und einer sehr überschaubaren Menge an HTML haben wir eine Lazy-Loading-Technik erstellt, die auch responsive Bilder berücksichtigt. Warum sind wir also nicht zufrieden?

Nun, wir haben zwei Reibungspunkte geschaffen:

  1. Unser Markup zum Hinzufügen von Bildern ist komplexer als zuvor. Das Leben war einfacher, als wir nur ein einziges img-Tag mit einem guten alten src-Attribut brauchten.
  2. Wir müssen auch mehrere Versionen jedes Bildassets erstellen, um jede Viewport-Größe und den Preload-Status zu füllen. Das ist mehr Arbeit.

Keine Sorge. Wir können beides optimieren.

Generierung der HTML-Elemente

Betrachten wir zuerst die Generierung des HTMLs, anstatt es von Hand zu schreiben.

Welches Werkzeug Sie auch immer zur Generierung Ihres HTMLs verwenden, es bietet wahrscheinlich eine Funktion für Includes, Funktionen, Shortcodes oder Makros. Ich bin ein großer Fan von solchen Helfern. Sie halten komplexere oder nuancierte Codefragmente konsistent und sparen Zeit, indem sie das Schreiben von langem Code überflüssig machen. Die meisten Static Site Generatoren bieten diese Möglichkeit.

  • Jekyll erlaubt Ihnen, benutzerdefinierte Plugins zu erstellen.
  • Hugo bietet Ihnen benutzerdefinierte Shortcodes.
  • Eleventy hat Shortcodes für alle von ihm unterstützten Template-Engines.
  • Es gibt noch viele mehr...

Als Beispiel habe ich in meinem Beispielprojekt, das mit Eleventy erstellt wurde, einen Shortcode namens lazypicture erstellt. Der Shortcode wird wie folgt verwendet:

{% lazypicture lighthouse.jpg "A snazzy lighthouse" %}

Um das benötigte HTML zur Build-Zeit zu generieren.

<picture class="lazy">
  <source srcset="/images/tiny/lighthouse.jpg" media="(min-width: 1200px)">
  <source srcset="/images/tiny/lighthouse.jpg" media="(min-width: 700px)">
  <source srcset="/images/tiny/lighthouse.jpg" media="(min-width: 300px)">
  <img src="/images/tiny/lighthouse.jpg" alt="A snazzy lighthouse" />
</picture>

Generierung der Bild-Assets

Der andere Aufwand, den wir uns selbst gemacht haben, ist die Generierung von Bild-Assets in verschiedenen Größen. Wir wollen nicht manuell jede Größe jedes Bildes erstellen und optimieren. Diese Aufgabe schreit nach Automatisierung.

Die Art und Weise, wie Sie dies automatisieren, sollte die Anzahl der benötigten Bild-Assets und die Häufigkeit, mit der Sie weitere Bilder hinzufügen, berücksichtigen. Sie könnten die Bilder als Teil jedes Builds generieren. Oder Sie könnten einige Bildtransformation-Services bei jeder Anfrage nutzen. Lassen Sie uns beide Optionen kurz betrachten.

Option 1: Bilder während Ihres Builds generieren

Beliebte Werkzeuge existieren dafür. Ob Sie Ihre Builds mit Grunt, Gulp, webpack, Make oder etwas anderem ausführen, es gibt wahrscheinlich ein Werkzeug für Sie.

Das folgende Beispiel verwendet gulp-image-resize in einer Gulp-Aufgabe als Teil eines Gulp-Build-Prozesses. Es kann ein Verzeichnis voller Bild-Assets verarbeiten und die benötigten Varianten generieren. Es bietet eine Vielzahl von Optionen zur Steuerung, und Sie können es mit anderen Gulp-Utilities kombinieren, um beispielsweise die verschiedenen Varianten gemäß Ihren Konventionen zu benennen.

var gulp = require('gulp');
var imageResize = require('gulp-image-resize');

gulp.task('default', function () {
  gulp.src('src/**/*.{jpg,png}')
    .pipe(imageResize({
      width: 100,
      height: 100
    }))
    .pipe(gulp.dest('dist'));
});

Die CSS-Tricks-Website verwendet einen ähnlichen Ansatz (dank der Funktion für benutzerdefinierte Größen in WordPress), um alle ihre verschiedenen Bildgrößen automatisch zu generieren. (Oh ja! CSS-Tricks lebt seine Philosophie!) ResponsiveBreakpoints.com bietet eine Web-UI, um mit verschiedenen Einstellungen und Optionen zum Erstellen von Bildsets zu experimentieren und generiert sogar den Code für Sie.

Oder Sie können es programmatisch nutzen, wie Chris auf Twitter erwähnte.

Wenn Sie jedoch so viele Bilddateien wie CSS-Tricks haben, kann die Durchführung dieser Arbeit als Teil eines Build-Schritts umständlich werden. Eine gute Caching-Strategie in Ihrem Build und andere Dateiverwaltungsaufgaben können helfen, aber es kann leicht zu einem langen Build-Prozess kommen, der Ihren Computer aufheizt, während er all die Arbeit verrichtet.

Eine Alternative ist, diese Ressourcen bei Bedarf zu transformieren, anstatt während eines Build-Schritts. Das ist die zweite Option.

Option 2: On-Demand-Bildtransformationen

Ich bin ein starker Befürworter der Vorab-Rendering von Inhalten. Ich habe diesen Ansatz (oft als JAMstack bezeichnet) schon seit einiger Zeit propagiert und glaube, dass er zahlreiche Vorteile in Bezug auf Performance, Sicherheit und Einfachheit bietet. (Chris fasste dies schön in einem Beitrag über Static Hosting und JAMstack zusammen.)

Dennoch mag die Idee, verschiedene Bildgrößen bei jeder Anfrage zu generieren, meinen Lazy-Loading-Zielen widersprechen. Tatsächlich gibt es heute eine Reihe von Diensten und Unternehmen, die sich darauf spezialisiert haben, und sie tun dies auf sehr leistungsfähige und bequeme Weise.

Die Kombination von Bildtransformationen mit leistungsfähigen CDN- und Asset-Caching-Funktionen von Unternehmen wie Netlify, Fastly und Cloudinary kann Bilder mit den von Ihnen über eine URL angegebenen Dimensionen schnell generieren. Jeder Dienst verfügt über erhebliche Verarbeitungsleistung, um diese Transformationen im laufenden Betrieb durchzuführen und die generierten Bilder für die zukünftige Verwendung zu cachen. Dies ermöglicht eine nahtlose Darstellung für nachfolgende Anfragen.

Da ich bei Netlify arbeite, werde ich dies anhand eines Beispiels mit dem Dienst von Netlify erläutern. Aber die anderen, die ich erwähnt habe, funktionieren auf ähnliche Weise.

Der Image Transformation Service von Netlify baut auf etwas auf, das Netlify Large Media genannt wird. Dies ist eine Funktion, die dazu dient, große Assets in Ihrer Versionskontrolle zu verwalten. Git ist standardmäßig nicht gut darin, aber Git Large File Storage kann Git erweitern, um die Einbeziehung großer Assets in Ihre Repositories zu ermöglichen, ohne sie zu überladen und unhandlich zu machen.

Sie können mehr über den Hintergrund dieses Ansatzes zur Verwaltung großer Assets lesen, wenn Sie interessiert sind.

Die Einbeziehung von Bildern unter Versionskontrolle in unsere Git-Repositories ist ein zusätzlicher Vorteil, aber für unsere Zwecke sind wir mehr daran interessiert, die Vorteile der On-the-Fly-Transformationen dieser Bilder zu nutzen.

Netlify sucht nach querystring-Parametern, wenn Bilder transformiert werden. Sie können die Höhe, Breite und die Art des Zuschnitts angeben, die Sie durchführen möchten. Wie hier:

  • Ein Rohbild ohne Transformationen
    /images/apple3.jpg
  • Ein Bild, das auf 300px Breite skaliert wurde
    /images/apple3.jpg?nf_resize=fit&w=300
  • Ein Bild, das als 500px mal 500px zugeschnitten wurde, mit automatischer Fokuspunkterkennung
    /images/apple3.jpg?nf_resize= smartcrop&w=500&h=500

Da wir wissen, dass wir jede Bildgröße aus einer einzigen Quellbilddatei in unserer Versionskontrolle erstellen und liefern können, muss der JavaScript-Code, den wir zum Aktualisieren der Bildquellen verwenden, nur die von uns gewählten Größenparameter enthalten.

Der Ansatz kann Ihre Website-Build-Prozesse drastisch beschleunigen, da die Arbeit nun ausgelagert und nicht mehr zur Build-Zeit ausgeführt wird.

Zusammenfassung

Wir haben hier viel Stoff behandelt. Es gibt viele gut erreichbare Optionen zur Implementierung von responsiven Bildern mit Lazy Loading. Hoffentlich gibt Ihnen dies genügend Informationen, um zweimal nachzudenken, bevor Sie zum nächstbesten Framework greifen, um Zugang zu dieser Art von Funktionalität zu erhalten.

Diese Demo-Website fasst eine Reihe dieser Konzepte zusammen und verwendet den Bildtransformationsservice von Netlify.

Noch einmal, um den Ablauf zusammenzufassen

  • Ein Static Site Generator mit einem Shortcode erleichtert die Erstellung der picture-Elemente.
  • Netlify Large Media hostet und transformiert die Bilder, liefert sie dann als winzige 20px breite Versionen aus, bevor die größeren Dateien bei Bedarf geladen werden.
  • CSS skaliert die winzigen Bilder hoch und weicht sie aus, um die Vorschau-Platzhalterbilder zu erstellen.
  • Die Intersection Observer API erkennt, wann die Bild-Assets durch die entsprechenden größeren Versionen ausgetauscht werden sollen.
  • JavaScript erkennt das load-Ereignis für die größeren Bilder und entfernt den Weichzeichner-Effekt, um das höher aufgelöste Rendering freizugeben.