Ein eigenes Promise-Wrapping-Pattern gegen Cumulative Layout Shift
Ein Bild, das erst nach dem restlichen Layout nachlaedt, verschiebt sichtbare Inhalte und verschlechtert den Cumulative-Layout-Shift-Wert. Mit React Suspense laesst sich das Rendern einer Bildkomponente gezielt zurueckhalten, bis die Bilddatei tatsaechlich im Browser-Cache liegt. Dieser Artikel zeigt ein eigenes Preload-Pattern auf Basis von Promise-Wrapping, inklusive Caching, Fehlerbehandlung und Grenzen des Ansatzes.
Inhaltsverzeichnis
- 1. Warum nachladende Bilder das Layout stoeren
- 2. Das Grundmuster: eine Suspense-taugliche Bildressource
- 3. Eine SuspenseImage-Komponente bauen
- 4. Layoutflaeche reservieren, bevor das Bild da ist
- 5. Fallback-Design: Platzhalter statt leerer Flaeche
- 6. Caching: doppeltes Laden und erneutes Suspendieren vermeiden
- 7. Fehlerbehandlung: Suspense und ErrorBoundary kombinieren
- 8. Mehrere Bilder parallel vorladen
- 9. Grenzen des Ansatzes und native Alternativen
- 10. Zusammenfassung
- 11. FAQ
1. Warum nachladende Bilder das Layout stoeren
Ein klassisches img-Tag reserviert im Browser erst dann Platz, wenn seine intrinsischen Masse bekannt sind, meist also erst nach dem Laden. Bis dahin nimmt das Element im Layout keinen oder einen falschen Raum ein, und sobald das Bild eintrifft, schieben sich darunterliegende Inhalte abrupt nach unten. Genau dieses Verhalten misst der Cumulative-Layout-Shift-Wert als einen der Core Web Vitals, und es wirkt sich direkt auf wahrgenommene Qualitaet und auf das Ranking aus.
React Suspense bietet einen strukturellen Ausweg: Statt ein img-Element sofort zu rendern und auf onLoad zu warten, laesst sich das Rendern der gesamten Bildkomponente so lange zurueckhalten, bis das Bild tatsaechlich geladen ist. Der Suspense-Fallback uebernimmt in der Zwischenzeit den reservierten Platz, das eigentliche Bild erscheint erst, wenn es ohne Nachladeruckeln direkt darstellbar ist. Voraussetzung ist ein eigenes Preload-Pattern, weil natives img-Loading von sich aus nicht mit Suspense kompatibel ist.
2. Das Grundmuster: eine Suspense-taugliche Bildressource
Suspense funktioniert, indem eine Komponente waehrend des Renderns eine Promise wirft, sofern die benoetigten Daten noch nicht vorliegen. React faengt diese Promise ab, zeigt den naechstgelegenen Fallback und versucht das Rendern erneut, sobald die Promise aufgeloest ist. Fuer Bilder bedeutet das: eine kleine Wrapper-Funktion muss ein natives Image-Objekt laden, den Ladezustand zwischenspeichern und beim Rendern entweder das Ergebnis zurueckgeben oder die laufende Promise werfen.
Diese sogenannte Resource kapselt drei Zustaende: pending, waehrend geladen wird, success, sobald die Bilddaten im Browser-Cache liegen, und error, falls das Laden fehlschlaegt. Wichtig ist, dass dieselbe Resource bei mehreren Rendern nicht erneut eine neue Promise erzeugt, sonst wuerde Suspense in eine Endlosschleife aus Fallback und erneutem Werfen geraten. Ein Cache pro Bild-URL loest dieses Problem zuverlaessig.
const imageCache = new Map();
function preloadImage(src) {
if (imageCache.has(src)) {
return imageCache.get(src);
}
let status = "pending";
let result;
const promise = new Promise((resolve, reject) => {
const img = new Image();
img.src = src;
img.onload = () => {
status = "success";
result = src;
resolve(src);
};
img.onerror = (err) => {
status = "error";
result = err;
reject(err);
};
});
const resource = {
read() {
if (status === "pending") throw promise;
if (status === "error") throw result;
return result;
},
};
imageCache.set(src, resource);
return resource;
}
3. Eine SuspenseImage-Komponente bauen
Auf Basis der Resource laesst sich eine kleine Komponente bauen, die im Render einfach resource.read() aufruft. Solange das Bild noch nicht geladen ist, wirft dieser Aufruf die zugrunde liegende Promise, React faengt sie ueber die umschliessende Suspense-Grenze ab und zeigt den Fallback. Ist das Bild fertig geladen, liefert read() die URL zurueck, und die Komponente rendert ein ganz normales img-Element, das jetzt garantiert sofort ohne sichtbares Nachladen erscheint.
Diese Komponente laesst sich wie jede andere Bildkomponente einsetzen, solange sie von einem Suspense-Element umschlossen wird. Der entscheidende Unterschied zu einem klassischen img mit onLoad-Handler ist, dass React das Rendern der Komponente selbst pausiert, statt nur eine bedingte Anzeige innerhalb der bereits gerenderten Komponente zu steuern. Das vereinfacht die Fallback-Logik erheblich, weil sie zentral auf Suspense-Ebene liegt statt in jeder einzelnen Bildkomponente dupliziert zu werden.
import { Suspense } from "react";
function SuspenseImage({ src, alt, width, height }) {
const resource = preloadImage(src);
resource.read();
return <img src={src} alt={alt} width={width} height={height} />;
}
function ProductImage({ src, alt }) {
return (
<Suspense fallback={<ImageSkeleton />}>
<SuspenseImage src={src} alt={alt} width={640} height={480} />
</Suspense>
);
}
4. Layoutflaeche reservieren, bevor das Bild da ist
Suspense allein verhindert noch keinen Layout Shift, es verschiebt das Problem nur zeitlich nach vorn: Wenn der Fallback weniger Platz einnimmt als das spaetere Bild, springt das Layout beim Wechsel von Fallback zu Bild trotzdem. Entscheidend ist deshalb, dass Fallback und finales Bild exakt dieselben Masse reservieren, etwa ueber feste width- und height-Attribute oder ueber eine CSS-aspect-ratio-Eigenschaft auf einem umschliessenden Container.
In der Praxis bewaehrt sich ein Container mit fixierter aspect-ratio, in dem sowohl der Skeleton-Fallback als auch das spaetere img-Element per absolute Positionierung denselben Raum ausfuellen. So bleibt die Seitenhoehe waehrend des gesamten Ladevorgangs konstant, unabhaengig davon, ob gerade der Fallback oder das fertige Bild sichtbar ist, und der Cumulative-Layout-Shift-Wert bleibt bei nahezu null fuer diesen Bereich der Seite.
5. Fallback-Design: Platzhalter statt leerer Flaeche
Ein leerer, grauer Kasten als Fallback ist funktional korrekt, wirkt aber unruhig, wenn viele Bilder gleichzeitig laden. Ein Skeleton mit dezenter Puls-Animation oder ein Low-Quality-Image-Placeholder, also eine winzige, stark komprimierte Vorschauversion des Bildes, vermitteln dem Nutzer, dass tatsaechlich etwas laedt, statt dass die Seite kaputt wirkt. Beide Varianten lassen sich als eigene Fallback-Komponente in die Suspense-Grenze einsetzen, ohne die eigentliche Lade-Logik zu beruehren.
Wichtig ist, den Fallback bewusst klein und leichtgewichtig zu halten, da er unter Umstaenden fuer jedes einzelne Bild einer Liste separat gerendert wird. Ein aufwendig animiertes Skeleton fuer eine Produktliste mit fuenfzig Eintraegen kann selbst zur Performance-Bremse werden, wenn es unnoetig viele DOM-Knoten oder teure CSS-Animationen erzeugt. Eine einfache CSS-Animation auf Basis von background-position reicht in der Regel voellig aus.
6. Caching: doppeltes Laden und erneutes Suspendieren vermeiden
Ohne Cache wuerde jede Neumontierung derselben Bildkomponente, etwa nach einer Navigation zurueck auf eine bereits besuchte Seite, das Bild erneut laden und die Komponente erneut suspendieren, obwohl der Browser die Datei laengst im HTTP-Cache vorhaelt. Die im vorherigen Abschnitt gezeigte Map mit der Bild-URL als Schluessel loest genau dieses Problem, weil sie den Ladezustand unabhaengig vom React-Lebenszyklus der Komponente vorhaelt.
Fuer Listen mit vielen Bildern lohnt es sich zusaetzlich, den Cache aktiv vorzuwaermen, etwa indem preloadImage bereits beim Hover ueber einen Link oder beim Eintreten in den sichtbaren Bereich aufgerufen wird, lange bevor die eigentliche SuspenseImage-Komponente gerendert wird. Dann liegt die Promise beim tatsaechlichen Rendern bereits im aufgeloesten Zustand, und der Nutzer sieht ueberhaupt keinen Fallback mehr, sondern sofort das fertige Bild.
7. Fehlerbehandlung: Suspense und ErrorBoundary kombinieren
Schlaegt das Laden eines Bildes fehl, etwa wegen eines toten Links oder eines Netzwerkfehlers, wirft die Resource in unserem Muster den Fehler statt der Promise. Suspense selbst faengt jedoch nur Promises ab, kein Fehlerobjekt; dafuer wird zusaetzlich eine klassische React-ErrorBoundary benoetigt, die den geworfenen Fehler abfaengt und stattdessen ein Ersatzbild oder eine Fehlermeldung anzeigt.
In der Praxis wird die ErrorBoundary direkt um die Suspense-Grenze gelegt, sodass ein fehlgeschlagenes Bild niemals die komplette umgebende Seite zum Absturz bringt, sondern nur den betroffenen Bildbereich betrifft. Fuer Produktbilder in einem Shop bietet sich als Fallback-Inhalt der ErrorBoundary ein generisches Platzhalterbild an, das dieselben width- und height-Masse reserviert wie das eigentlich erwartete Bild, um erneute Layout-Spruenge zu vermeiden.
class ImageErrorBoundary extends React.Component {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return <PlaceholderImage width={this.props.width} height={this.props.height} />;
}
return this.props.children;
}
}
function SafeProductImage({ src, alt, width, height }) {
return (
<ImageErrorBoundary width={width} height={height}>
<Suspense fallback={<ImageSkeleton width={width} height={height} />}>
<SuspenseImage src={src} alt={alt} width={width} height={height} />
</Suspense>
</ImageErrorBoundary>
);
}
8. Mehrere Bilder parallel vorladen
Bei einer Bildergalerie oder einem Produktkarussell mit mehreren Bildern will man in der Regel nicht, dass jedes Bild nacheinander suspendiert und damit eine Wasserfall-Ladereihenfolge entsteht. Da die Browser-Netzwerkanfragen ohnehin parallel laufen koennen, reicht es, alle benoetigten preloadImage-Aufrufe vor dem eigentlichen Rendern auszuloesen, etwa in einem uebergeordneten Effekt oder direkt beim Zusammenstellen der Bildliste, statt sie erst innerhalb jeder einzelnen SuspenseImage-Instanz zu starten.
Eine gemeinsame Suspense-Grenze um die gesamte Galerie sorgt zusaetzlich dafuer, dass entweder alle Bilder gemeinsam erscheinen oder gemeinsam der Fallback sichtbar bleibt, was ein saubereres Nutzererlebnis erzeugt als ein unkoordiniertes Einzeln-Auftauchen der Bilder. Fuer sehr viele Bilder empfiehlt sich hingegen eine einzelne Suspense-Grenze pro sichtbarem Ausschnitt, damit nicht das gesamte Karussell auf das langsamste Bild warten muss.
9. Grenzen des Ansatzes und native Alternativen
Das Suspense-Preload-Pattern ist kein Ersatz fuer grundlegende Bildoptimierung: responsive Bildgroessen ueber srcset, moderne Formate wie WebP oder AVIF und ein sinnvoller loading-Wert bleiben weiterhin notwendig. Suspense loest gezielt das Problem des Rendering-Timings, nicht die Dateigroesse oder das Format der Bilder selbst, und sollte deshalb als Ergaenzung, nicht als Ersatz fuer diese Grundlagen verstanden werden.
Fuer above-the-fold-Bilder, die sofort sichtbar sein muessen, ist fetchpriority="high" in Kombination mit einem serverseitig vorgerenderten Platzhalter oft die einfachere Loesung, weil das Suspense-Pattern client-seitiges JavaScript voraussetzt. Fuer Bilder unterhalb des sichtbaren Bereichs, insbesondere in dynamisch nachgeladenen Listen, spielt das gezeigte Muster seine Staerken dagegen voll aus, weil es Layout-Stabilitaet und kontrolliertes Nachladeverhalten ohne zusaetzliche Bibliotheken vereint.
| Ansatz | Verhindert CLS | Aufwand | Am besten geeignet fuer |
|---|---|---|---|
| Natives img ohne Masse | Nein | Keiner | Nicht empfohlen |
| width/height-Attribute | Ja, bei bekannten Massen | Gering | Statische Bilder mit fester Groesse |
| CSS aspect-ratio + Skeleton | Ja | Mittel | Responsive Bilder in Karten/Listen |
| Suspense-Preload-Pattern | Ja, plus kontrolliertes Rendering | Mittel bis hoch | Galerien, dynamische Listen, SPA-Navigation |
| fetchpriority + SSR-Platzhalter | Ja | Gering bis mittel | Above-the-fold Hero-Bilder |
Mironsoft
React-Architektur, Performance und Magento-Frontend-Integration
React-Frontends, die schnell bleiben statt mit jedem Feature langsamer zu werden?
Wir prüfen bestehende React-Anwendungen auf unnötige Re-Renders, aufgeblähte Bundles und fragile State-Verwaltung und bauen daraus ein Frontend, das performant bleibt und sich sauber an Magento oder andere Backends anbindet.
Performance-Audit
Re-Renders, Bundle-Größe und Ladezeiten systematisch messen und beheben.
State-Architektur
Context, Zustand und Server State sauber trennen statt alles in einen Topf zu werfen.
Magento-Integration
GraphQL- oder REST-Anbindung an Magento robust und typsicher aufbauen.
10. Zusammenfassung
Suspense-Bildvorladen: Das Wichtigste auf einen Blick
Grundprinzip
Eine Komponente wirft eine Promise, bis das Bild geladen ist, Suspense zeigt solange den Fallback.
Layout-Stabilitaet
Fallback und finales Bild muessen exakt denselben Platz reservieren, sonst verschiebt sich das Layout trotzdem.
Caching
Eine URL-basierte Ressourcen-Map verhindert doppeltes Laden und erneutes Suspendieren bei Remounts.
Grenzen
Ersetzt keine Bildoptimierung wie srcset oder moderne Formate, ergaenzt sie um kontrolliertes Rendering-Timing.