Eine der größten Herausforderungen komplexer Webseiten ist das Management von Lade- und Renderingzeiten unter zeitkritischen Bedingungen. Dateikomprimierung und Caching helfen oftmals weiter und auch die Reduzierung von Stylesheets und Scripten auf das initial notwendige Minimum können bereits viel erreichen. Manchmal kommt man aber nicht um asynchrone Ladevorgänge herum. Dabei wird ein Teil der erforderlichen Daten erst abgerufen, wenn die Webseite im Wesentlichen bereits geladen wurde. Hier gibt es viele unterschiedliche Szenarien und passende Lösungsansätze, die alle ihre Vor- und Nachteile haben. Oftmals geht es nur darum, auf bildlastigen Seiten Vorschaubilder anzubieten, die mit den vollaufgelösten Abbildungen ersetzt werden, sobald diese verfügbar sind.

Damit vermeidet man einerseits das nervige Content Jumping, bei dem Seitenelemente im Browser teilweise mehrfach verschoben werden, weil weiter oben ein neues Element nachgeladen wurde und sich nun Platz verschafft. Im Falle von img-Tags lässt sich dieses Problem leicht mit der Angabe von width– und height-Attributen umgehen. Sie sollten ohnehin immer angegeben werden und können jederzeit durch globale oder inline-Styles überschrieben werden.

<img src="http://..." alt="Ein Bild" width="400" height="300">

Schöner und sicherlich auch für die User Experience besser ist es jedoch, statt der sich langsam im Dokument schließenden Lücke ein Vorschaubild anzubieten, das erst ausgewechselt wird, wenn das eigentliche Bild vollständig heruntergeladen wurde. Das ist natürlich nur so lange sinnvoll, wie die Vorschaubilder wesentlich kleiner sind und schneller heruntergeladen werden können. Auf manchen Webseiten wird dafür eine nur wenige Pixel kleine Version des eigentlichen Bildes verwendet, welche einfach über die gesamte Fläche ausgebreitet wird und somit wie eine verschwommene Vorschau aussieht.

Frost-Covered Dunes in Crater, © NASA/JPL/University of Arizona
Dieses Vorschaubild von 10×8 Pixeln Größe wird siebzigfach vergrößert und dabei durch browsereigene Algorithmen automatisch verschwommen dargestellt.

Das ästhetische Ergebnis sieht bereits um einiges besser aus. Die Methode führt jedoch auf Webseiten mit vielen Bildern zu längeren Ladezeiten. Weniger, weil mehr Daten übertragen müssen, sondern weil die vielen zusätzlichen HTTP-Requests zusätzliche Wartezeiten zwischen Browser und Server mit sich bringen.

Die Bilddaten bereits im HTML-Dokument mitliefern

Ideal wäre es, wenn die notwendigen Daten da schon im HTML-Dokument enthalten wären. Ich meine nicht, dass die URLs der Vorschaubilder ebenfalls angegeben werden, sondern dass ihr Dateiinhalt bereits im HTML-Dokumentiert mitgeliefert wird. Ein typisches img-Tag auf dieser Webseite sieht dann so aus:

<img src="http://…" alt="Ein Bild" width="400" height="300" data-preload-img="3klEQVQImQ…">

Das zusätzliche data-preload-img-Attribut enthält dabei alle notwendigen Daten, um ein verschwommenes Bild generieren zu können. Typischerweise wird auf diese Weise eine Zeichenkette von etwa 300 Bytes übertragen, was auch bei einer Vielzahl von derart erweiterten img-Tags nicht großartig ins Gewicht fällt.

Praktischerweise können solche Daten-URIs in CSS verarbeitet und etwa für den Hintergrund eines Elements eingesetzt werden. Für kleine Grafiken, etwa Icons in einem Menü oder eben klein Vorschaubilder, bietet sich diese Technik an, da sie eben HTTP-Requests spart. Die Notation ist denkbar einfach:

div {
	background-image: url(data:image/png;base64,3klEQVQImQ…);
}

Der url-Ausdruck, der sonst Dateipfade aufnimmt, kann auch mit dem Schlüsselwort data, dann dem MIME-Typ der Grafik und schließlich dem Namen des Kodierungsalgorithmus versehen werden. Anschließend folgt der Code.

Wie mit dieser Technik ein ordentliches Bild entsteht und auf diese Weise der Ladeprozess angenehmer gestaltet wird, erkläre ich im folgenden Tutorial.

Eine Animation des Ladevorgangs

Lazy Loading in JavaScript und PHP

Um nach diesem Prinzip Vorschaubilder einzubinden, müssen wir diese zunächst generieren. Das geschieht am besten serverseitig. Hierzu wird auf Vorschaubild von nicht mehr als 10×8 Pixeln (oder ähnlich klein) zugegriffen. Größere Bildmengen, die ein händisches Erstellen all dieser Miniaturen nicht erlauben, können auch automatisiert verkleinert werden. In PHP steht etwa die Funktion imagescale() bereit und WordPress-User können mit add_image_size() einfach eine neue Bildgröße generisch erstellen lassen. Wie auch immer wir an die kleinen Formate gelangen, müssen wir sie anschließend so kodieren, dass wir sie in das HTML-Dokument einbetten können. Base64 bietet sich hierzu an, da wir die auf diese Weise verschlüsselten Bilddaten später in CSS einfügen können und der Browser daraus das enthaltene Bild erstellt. In PHP können wir auf die Funktion base64_encode zurückgreifen:

$data = file_get_contents('pfad/zum/kleinenbild.png');
$base64 = base64_encode($data);

Im diesem Code-Beispiel wird erst eine PNG-Datei eingelesen, ihr Dateiinhalt in der Variable $data gespeichert und anschließend Base64-kodiert. Wer auf seinem Server nicht PHP, sondern eine andere Sprache nutzt, findet in aller Regel bereits implementierte Base64-Kodierungsfunktionen. Beispielsweise würde man in Node.js schreiben: Buffer.from('…dateiinhalt…').toString('base64'). Auch die folgenden Schritte in PHP lassen sich im Wesentlichen in alle üblichen Serversprachen übersetzen.

Da alle Base64-kodierten PNG-Dateien mit einer bestimmten Zeichenfolge beginnen, können wir diese aus dem String entfernen, so noch mehr Zeichen sparen, um sie dann später in JavaScript wieder einzufügen:

$base64 = str_replace('iVBORw0KGgoAAAANSUhEUgAAAAoAAAA', '', $base64);

Der einmal derart generierte Base64-kodierte String muss dann nur noch in das img-Tag eingefügt werden:

<img src="htp://…" alt="Ein Bild" width="400" height="300" data-preload-img="<?php echo $base64; ?>">

In JavaScript werden dann zunächst alle img-Tags mit einem data-preload-img-Attribut ausgelesen und in einer Schleife einzeln behandelt. Danach werden die Breite, die Höhe und der Inhalt des Attributs bestimmt.

var images = document.querySelectorAll('img[data-preload-img]');
for (i = 0; i < images.length; i++) {
	let image = images[i], width, height, css;

	width = (image.offsetWidth > 0) ? image.offsetWidth : image.getAttribute('width');
	height = (image.offsetHeight > 0) ? image.offsetHeight : (width * image.getAttribute('height') / image.getAttribute('width'));
	base64 = image.getAttribute('data-preload-img');
	css = 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAA' + base64;

Im Anschluss wird das img-Tag in ein neues Container-Element mit der Klasse image-preloader eingefügt. Seine Abmessungen werden an das Bild angepasst, ehe sein Hintergrund mit dem Vorschaubild gefüllt wird:

	let container = document.createElement('div');
	container.className = 'image-preloader';
	image.parentNode.insertBefore(container, image);
	container.appendChild(image);

	container.style.width = width + 'px';
	container.style.height = height + 'px';
	container.style.backgroundImage = css;

	replaceImage(container, image);
}

Währenddessen bleibt das eigentliche img-Tag so lange leer, bis seine Bildquelle vollständig geladen wurde. In dieser Zeit wird lediglich der Container und sein Hintergrund angezeigt. Sobald der Container mit dem Vorschaubild vorbereitet ist, wird die Funktion replaceImage() aufgerufen, welche sowohl das Container- als auch Image-Element als Argumente übernimmt.

replaceImage() wird nun für jedes Bild und seinen Container einzeln aufgerufen. Dazu wird zunächst eine Variable shadowImg als ein virtuelles Image-Element definiert, welches dann das eigentliche Bild aufnimmt und, sobald dieses geladen ist, das onload-Event feuert

const replaceImage = (container, image) => {
	let shadowImg = new Image();
	shadowImg.onload = () => {
		container.className += ' loaded';
		setTimeout(() => {
			let parent = container.parentNode;
			while (container.firstChild) parent.insertBefore(container.firstChild, container);
			parent.removeChild(container);
			image.removeAttribute('data-preload-img');
		}, 2000);
	}
	shadowImg.src = image.getAttribute('src');
}

Sobald dies geschehen ist, wird zunächst dem Container eine neue Klasse vergeben, um ihn später in CSS gesondert ansprechen zu können. Außerdem warten wir zwei Sekunden, ehe das img-Tag von seinem zeitweiligen Container und seinem data-preload-img-Attribut befreit wird.

Der Grund für die Definition einer neuen Funktion replaceImage() liegt in der asynchronen Natur des onload-Events: Wenn wir mehrere Vorschaubilder wie oben in einer for-Schleife abhandeln, wird das onload-Event mitunter erst gefeuert, wenn die Schleife bereits längst abgelaufen ist. Das hätte zur Folge, dass nur das letzte Vorschaubild im DOM-Baum ordentlich ersetzt wird, während alle anderen nie durch ihre eigentlichen Bilder ersetzt werden. Auf diese Weise können wir allerdings sicherstellen, dass auch wirklich jedes Bild geladen wird. Diese Lösung funktioniert aber nur, wenn die Funktion replaceImage() definiert wurde, ehe sie aufgerufen wird. Daher ist es am besten diesen gesamten Block direkt vor der for-Schleife einzufügen.

Nun fehlen nur noch die Styles: In CSS breiten wir einerseits den Hintergrund auf die volle Fläche aus und animieren andererseits den Übergang vom verschwommenen Vorschaubild zum geladenen Bild:

.image-preloader {
	background-size: cover;
	background-repeat: no-repeat;
	background-position: 50% 50%;
}

.image-preloader img {
	opacity: 0;
	-webkit-transition: opacity 1s ease;
	   -moz-transition: opacity 1s ease;
	     -o-transition: opacity 1s ease;
	        transition: opacity 1s ease;
}

.image-preloader.loaded img {
	opacity: 1;
}
Frost-Covered Dunes in Crater, © NASA/JPL/University of Arizona

Anpassungen für andere Dateiformate

Damit ist der Lazy Loader bereits fertig. In dieser Form ist er allerdings nur für PNG-Dateien ausgelegt. Andere Bildformate können analog kodiert werden, jedoch müssen dann JavaScript-seitig andere Header für den data-uri-String des CSS-Hintergrund-Stils gewählt werden. Außerdem beginnen Base64-kodierte Strings für andere Bildformate mit anderen Zeichenketten. Man könnte an dieser Stelle sowohl Server- als auch Client-seitig switch-Statements einführen oder von vornherein alle Vorschaubilder in PNG konvertieren. Dem eingangs gezeigten Code-Beispiel würden dann folgende Zeilen vorangestellt werden:

$type = pathinfo($pfadzumvorschaubild, PATHINFO_EXTENSION);
$file = file_get_contents($pfadzumvorschaubild);
if ($type === 'png') {
	$data = $file;
} else {
	ob_start();
	imagepng(imagecreatefromstring($file));
	$data = ob_get_contents();
	ob_end_clean();
}

Die in allen gängigen PHP-Versionen enthaltene Bildverarbeitungsbibliothek GD hält eine Funktion imagepng() bereit, mit der aus beliebigen Ausgangsformaten PNG-Dateien erstellt werden können. Da wir das Ergebnis nicht erst in einer Datei speichern und dann wieder auslesen möchten, nutzen wir durch Auslassen des Parameters „to“ die Möglichkeit, es als Datenstrom auszugeben. Dazu müssen wir allerdings zuvor mit ob_start() die Ausgabepufferung aktivieren, um den Datenstrom abfangen zu können (sodass er nicht vorzeitig im Browser landet). Danach können wir mit ob_get_contents() die Daten auslesen und der Variable $data übergeben.

Hinterlasse einen Kommentar