Lazy Loading von Images mit Hilfe von Custom Elements

Durch responsive Layouts und hochauflösende Displays sind die Anforderungen an Grafikdateien auf Webseiten gestiegen. Hinzu kommt, dass innerhalb einer einzigen Webseiten mitlerweile oft mehrere hundert Grafiken geladen werden. Abhilfe schafft Lazy Loading von Ressourcen z. B. Bildern (img-Tag).
Je nachdem wie weit der Besucher einer Webseite nach unten scrollt bzw. dies nicht tut, bekommt er diese Grafiken nie zu sehen. Sie wurden also völlig umsonst geladen.
Das Nachladen von Grafikdateien erst dann, wenn sie sich innerhalb des sichtbaren Bereichs liegen, macht also durchaus Sinn. Es dient der Performance und kann das Datenvolumen bei mobilen Geräten schonen.

Was ist Lazy Loading?

Lazy Loading ist eine Technik, die das Laden nicht kritischer Ressourcen zum Zeitpunkt des Ladens der Seite aussetzt. Stattdessen werden sie im Moment des Bedarfs geladen. Bei Grafiken ist dieses "nicht kritisch" meist gleichbedeutend mit Grafiken, die außerhalb des Viewports des Benutzers liegen.

Custom Elements

Hier sollen nun die Quelldateien des img-Tags erst geladen werden, wenn das jeweilige Bild im oder in der Nähe des Viewports des Webseitenbesuchers liegt. Für die Umsetzung eignen sich Custom Elements. Custom Elements sind Webkomponenten (web components). Diese sind benutzerdefinierte HTML-Tags zur Verwendung in Webseiten bzw. Webanwendungen, funktionieren in allen modernen Browsern und basieren auf vorhandenen Webstandards. Für ältere Browser gibt es Polyfills, die nur dann ausgeliefert werden, wenn der Browser Webkomponenten noch nicht integriert hat.
Custom Elements müssen mindestens einen Bindestrich "-" enthalten und man sollte nicht data-* verwenden, da dies für data- Attribute reserviert ist. Ist ein Custom Element einmal dem Browser bekannt gemacht worden, kann es wie jedes andere HTML-Tag auch asynchron nachträglich ins DOM eingefügt werden.

In meinem Fall bedeutet dies nun, dass das Custom Element tn-image mit dem Browser bekannt gemacht werden muss.
Innerhalb dieses Elements wird dann ein herkömmliches img Element dargestellt, wobei das src Attribut erst geladen wird, wenn sich das Element innerhalb oder ganz nah am Viewport des Besuchers befindet.
Für den Fall, dass ein Seitenbesucher in seinem Browser JavaScript deaktiviert haben sollte, wird innerhalb eines noscript Tags ebenfalls ein ganz normales img Element ausgegeben.


<!-- webcomponents bundle mit allen benötigten polyfills laden -->
<script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script>

<script>
<!-- Element definieren -->
class TnImg extends HTMLElement {
  constructor() {
    super();
  };

  connectedCallback() {
    let src = this.getAttribute('src');
    let alt = this.getAttribute('alt');
    this.innerHTML = '<img class="lazy"' + (src ? ' data-src="' + src + '"' : '') + (alt ? ' alt="' + alt + '"' : '') + ' />';
  };
}

window.customElements.define('tn-img', TnImg);
</script>

<!-- Element kann nun benutzt werden -->
<tn-img src="test.jpg" alt="Testbild"></tn-img>

<!-- Fallback für Besucher ohne JavaScript -->
<noscript><img src="test.jpg" alt="Testbild"</noscript>

Jedes Custom Element tn-img beinhaltet nun ein img Element mit dem Attribut data-src, welches den Pfad zu der Bild-Datei beinhaltet.

Das optimale Bild für das jeweilige Endgerät laden

Der Vorteil des Custom Elements gegenüber dem ganz normalen img Tag liegt nun darin, dass man über JavaScript weitere Informationen über das Endgerät, die Auflösung und die zur Verfügung stehende Breite abfragen kann, um dem Benutzer die perfekte Quelldatei auszuliefern. Man ist nicht auf Media-Queries beschränkt.
Der Nachteil von Media-Queries ist, dass sie sich immer auf den Viewport beziehen. Für ein Bild innerhalb der Sidebar greift bspw. der selbe Media-Query wie für ein auf voller Breite dargestelltes Bild.

Hier kann beispielsweise einem Seitenbesucher, der über ein Smartphone auf die Webseite zugreift, eine Bilddatei mit einer geringen Qualität und Auflösung ausgeliefert werden, um eventuell sein mobiles Datenvolumen zu schonen.

Das Lazy Loading implementieren

Ob sich nun ein Bild innerhalb oder in der Nähe des Viewports des Benutzer befindet und geladen werden soll, prüfe ich in meinem Fall nicht über die Intersection Obeserver API, sondern über Eventhandler, da diese in allen gängigen Browsern unterstützt wird.


document.addEventListener('DOMContentLoaded', function() {
  let myLazyImages = [].slice.call(document.querySelectorAll('img.lazy'));
  let running = false;
  let margin = 100;

  const lazyLoadingTimeout = function() {
    if (running === false) {
      running = true;

      setTimeout(function() {
        lazyLoading();
        running = false;
      }, 250);
    }
  };

  const lazyLoading = function() {
	myLazyImages.forEach(function(lazyImage) {
	  if (lazyImage.getBoundingClientRect().top <= window.innerHeight + margin && lazyImage.getBoundingClientRect().bottom >= 0 && getComputedStyle(lazyImage).display !== 'none') {
		lazyImage.src = lazyImage.dataset.src;
		lazyImage.classList.remove('lazy');

		myLazyImages = myLazyImages.filter(function(image) {
		  return lazyImage !== image;
		});

		if (myLazyImages.length === 0) {
		  window.removeEventListener('orientationchange', lazyLoadingTimeout);
		  document.removeEventListener('scroll', lazyLoadingTimeout);
		  window.removeEventListener('resize', lazyLoadingTimeout);
		}
	  }
	});
  
  };

  window.addEventListener('orientationchange', lazyLoadingTimeout);
  document.addEventListener('scroll', lazyLoadingTimeout);
  window.addEventListener('resize', lazyLoadingTimeout);
  lazyLoading();
});

In der Variable margin ist der Abstand eines Bildes zum Viewport des Benutzers hinterlegt, bei dem das Bild geladen werden soll, obwohl es sich noch nicht innerhalb des Viewports befindet.
Ein setTimeout wird hier verwendet, um den Prozesser nicht zu sehr zu belasten. Alle bereits geladenen Bilder werden aus dem Array entfernt, nachdem sie geladen wurden. Sobald alle Bilder geladen wurden, wird der Eventhandler entfernt.