Suspense fuer Bilder: Vorladen ohne Layout Shift in React 19
AI generated
{ }
React · Performance · UX
Bilder mit Suspense vorladen und Layout Shifts vermeiden
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.

13 Min. Lesezeit Suspense Performance CLS Bilder React 19

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.

11. FAQ: Suspense-Bildvorladen: Das Wichtigste auf einen Blick

1Funktioniert das Suspense-Preload-Pattern auch mit Server-Side Rendering?
Nur eingeschraenkt, weil das Werfen von Promises waehrend des Renderns clientseitiges Verhalten voraussetzt. Fuer serverseitig gerenderte Seiten empfiehlt sich stattdessen ein bereits bekanntes Bild direkt beim ersten Rendern auszuliefern und Suspense nur fuer nachtraeglich client-seitig geladene Bilder einzusetzen.
2Kann ich denselben Cache fuer mehrere Bildkomponenten im Projekt teilen?
Ja, ein modulweiter Map-Cache wie im gezeigten Beispiel wird automatisch von allen Komponenten geteilt, die dieselbe preloadImage-Funktion importieren. Das verhindert doppeltes Laden derselben Bild-URL an verschiedenen Stellen der Anwendung.
3Was passiert, wenn ein Bild nie laedt, weil die URL falsch ist?
Die zugrunde liegende Image-Instanz loest irgendwann onerror aus, die Resource wirft dann den Fehler statt weiter zu pending zu bleiben. Eine umgebende ErrorBoundary faengt diesen Fehler ab und kann einen Platzhalter oder eine Fehlermeldung anzeigen.
4Muss ich fuer jedes Bild eine eigene Suspense-Grenze setzen?
Nicht zwingend, mehrere Bilder koennen sich eine gemeinsame Suspense-Grenze teilen, wodurch sie gemeinsam erscheinen statt einzeln nachzuladen. Die Wahl haengt davon ab, ob ein koordiniertes oder ein unabhaengiges Erscheinen der Bilder gewuenscht ist.
5Verursacht das Werfen von Promises Performance-Probleme?
Nein, das Werfen einer Promise waehrend des Renderns ist ein von React offiziell unterstuetzter Mechanismus fuer Suspense und verursacht keinen spuerbaren Overhead. Wichtig ist nur, dieselbe Promise nicht bei jedem Render neu zu erzeugen, wofuer der gezeigte Cache sorgt.
6Wie unterscheidet sich das von React Querys Suspense-Modus?
React Query bietet einen eingebauten Suspense-Modus fuer Datenabfragen mit eigenem Caching, Retry-Logik und Invalidierung. Fuer Bilder laesst sich ein aehnliches, aber deutlich schlankeres Muster von Hand bauen, wie in diesem Artikel gezeigt, ohne eine zusaetzliche Abhaengigkeit einzufuehren.
7Lohnt sich der Aufwand fuer eine einzelne Produktseite mit einem Bild?
Fuer ein einzelnes, above-the-fold-Bild lohnt sich meist die einfachere Loesung ueber width/height-Attribute und fetchpriority. Das Suspense-Pattern zahlt sich vor allem bei Listen, Galerien und dynamisch nachgeladenen Bildern mehrfach aus.
8Kann ich das Pattern auch fuer Hintergrundbilder statt img-Elemente nutzen?
Ja, die preloadImage-Funktion selbst kennt kein img-Element, sie laedt lediglich die Bilddaten vor. Eine Komponente kann resource.read() genauso nutzen, um anschliessend ein background-image per Inline-Style zu setzen, statt ein img-Tag zu rendern.
9Wie gehe ich mit sehr vielen Bildern in einer unendlich scrollenden Liste um?
Hier empfiehlt sich, preloadImage erst auszuloesen, wenn ein Bild kurz vor dem sichtbaren Bereich steht, etwa ueber einen IntersectionObserver, statt alle Bilder der Liste sofort vorzuladen. So bleibt die Anzahl gleichzeitiger Netzwerkanfragen kontrollierbar.
10Ist dieses Pattern mit React 19 Server Components kompatibel?
Server Components selbst laden Daten anders und benoetigen dieses clientseitige Promise-Werfen nicht. Das Pattern eignet sich fuer Client Components, die explizit mit 'use client' markiert sind und im Browser interaktiv Bilder nachladen.