Bilder in React kommen entweder als Import aus dem /src-Verzeichnis (vom Bundler verarbeitet, mit hash-basiertem Cache-Busting) oder als direkter Pfad aus dem /public-Verzeichnis (unverändert ausgeliefert). Die Wahl bestimmt, wie der Bundler die Datei behandelt, wie sich der finale URL-Pfad zusammensetzt und wie Caching greift. Dazu kommen die üblichen HTML-Themen: das alt-Attribut für Accessibility, loading="lazy" für Performance, srcset für responsive Bilder, und das Behandeln defekter Bilder mit onError.
Zwei wichtige Regeln
- Der
<img />-Tag muss in JSX selbstschließend sein (oder mit explizitem</img>-Close — selbstschließend ist üblicher). - Lokale Bilder aus
/srcmüssen importiert werden, damit der Build-Prozess sie erkennt und in der finalendist/-Struktur korrekt platziert.
import bild from './assets/foto.jpg';
const MinimalImage = () => {
return <img src={bild} alt="Beispielfoto" />;
};Import aus /src — vom Bundler verarbeitet
Build-Tools wie Vite, Webpack oder Parcel transformieren Bilder, die per import referenziert werden: sie kopieren die Datei in den Output-Ordner, hängen einen Content-Hash an den Dateinamen (foto.a1b2c3.jpg) und ersetzen den Import-Wert durch den finalen URL.
import sampleImage from '../../assets/sample_image.jpg';
const ImageImport = () => {
return (
<div className="sample-image-wrapper">
<img src={sampleImage} alt="Beispielbild" />
</div>
);
};
export default ImageImport;Vorteile dieses Pattern:
- Cache-Busting: Hash im Dateinamen → Browser lädt das Bild bei Änderungen neu.
- Tree-Shaking: Nicht importierte Bilder landen nicht im Bundle.
- Inline für kleine Bilder: Bei Bedarf inlinet der Bundler kleine Bilder als Data-URLs (konfigurierbar).
Die generierten Dateinamen ändern sich pro Build:

Direkter Pfad aus /public
Dateien im public/-Ordner werden unverändert in das Build-Output kopiert. Der Pfad ist absolut und stabil — kein Import nötig.
const ImagePublic = () => {
return <img src="/images/react_project.jpg" alt="React Projekt" />;
};Wann /public statt /src?
- Bilder, die dynamisch per externer URL/CMS gewählt werden und nicht vorab bekannt sind.
- Dateien, die mit stabilem Pfad referenziert werden müssen (z.B.
og:image-Meta-Tags, favicon). - Assets, die explizit ohne Hash ausgeliefert werden sollen (z.B.
robots.txt,sitemap.xml).
Nachteil: kein Cache-Busting bei Änderungen — Browser cachen das alte Bild ggf. lange.
alt-Attribut — Accessibility-Pflicht
Jedes <img> braucht ein alt-Attribut. Es beschreibt das Bild für Screen-Reader und wird angezeigt, wenn das Bild nicht lädt.
// Inhaltliches Bild: aussagekräftiges alt
<img src={logo} alt="mibeon Logo" />
// Dekoratives Bild: leerer alt — Screen-Reader überspringt
<img src={ornament} alt="" />ESLint mit eslint-plugin-jsx-a11y warnt bei fehlendem alt. Faustregel:
- Inhaltliches Bild: kurze, aussagekräftige Beschreibung.
- Dekoratives Bild:
alt=""(leer, aber vorhanden). - Bild als Link: das
altbeschreibt das Ziel des Links.
Lazy Loading
Mit loading="lazy" lädt der Browser ein Bild erst, wenn es in den Viewport scrollt — Performance-Boost auf bilderlastigen Seiten.
const Gallery = ({ images }) => {
return (
<div>
{images.map((img) => (
<img
key={img.id}
src={img.url}
alt={img.title}
loading="lazy"
width={400}
height={300}
/>
))}
</div>
);
};width/height sind bei lazy-loaded Bildern wichtig — sie reservieren den Platz im Layout und vermeiden Cumulative Layout Shift (CLS, ein Core Web Vital).
Responsive Bilder mit srcset
Für mobile-vs-desktop unterschiedliche Bilder liefert srcset dem Browser eine Auswahl, aus der er die passende Auflösung wählt.
import small from './hero-small.jpg';
import medium from './hero-medium.jpg';
import large from './hero-large.jpg';
const Hero = () => {
return (
<img
src={medium}
srcSet={`${small} 480w, ${medium} 800w, ${large} 1200w`}
sizes="(max-width: 600px) 480px, (max-width: 1024px) 800px, 1200px"
alt="Hero-Bild"
/>
);
};Beachte: in JSX heißt das Attribut srcSet (camelCase), nicht srcset. Build-Tools können das srcset auch automatisch generieren (z.B. vite-imagetools).
Dynamische Bilder — Import in der Schleife
Wenn die Datei-Namen erst zur Laufzeit feststehen, kann man Bilder nicht per import einbinden. Drei Optionen:
// Option 1: alle Bilder vorab importieren, dann per Schlüssel auswählen
import imgA from './a.jpg';
import imgB from './b.jpg';
const imageMap = { a: imgA, b: imgB };
const ImageA = ({ name }) => <img src={imageMap[name]} alt="" />;
// Option 2: import.meta.glob (Vite-spezifisch) — alle passenden Dateien auf einmal
const modules = import.meta.glob('./images/*.jpg', { eager: true });
const images = Object.fromEntries(
Object.entries(modules).map(([path, mod]) => [path, mod.default])
);
// Option 3: /public und direkter String-Pfad
const PublicImage = ({ name }) => (
<img src={`/images/${name}.jpg`} alt="" />
);Variante 1 funktioniert mit beliebigem Bundler. Variante 2 ist Vite-spezifisch. Variante 3 verzichtet auf Bundler-Optimierung, aber ist einfach.
Defekte Bilder behandeln
Mit onError lässt sich auf Lade-Fehler reagieren — z.B. Fallback-Bild oder Placeholder.
import fallback from './fallback.png';
const SafeImage = ({ src, alt }) => {
const handleError = (e) => {
e.currentTarget.src = fallback;
// Wichtig: onError abklemmen, sonst Endlos-Loop falls auch fallback nicht lädt
e.currentTarget.onerror = null;
};
return <img src={src} alt={alt} onError={handleError} />;
};Häufige Stolperfallen
Bilder in /src — IMMER importieren, nie als String-Pfad.
<img src="../assets/bild.jpg" /> funktioniert in Dev evtl. zufällig, scheitert aber in Production — der Bundler kennt den String nicht und kopiert das Bild nicht ins Build-Output. Lösung: import bild from './bild.jpg'.
alt-Attribut ist Pflicht — kein Auslassen.
Fehlt das alt, warnt ESLint mit jsx-a11y/alt-text. Inhaltliche Bilder bekommen eine sinnvolle Beschreibung, dekorative bekommen alt="". Niemals einfach weglassen.
loading='lazy' erst seit nativer Browser-Unterstützung sinnvoll.
Native lazy-loading ist in Chrome, Firefox, Safari seit ~2020 verfügbar. Davor brauchte man Intersection Observer oder Library-Lösungen. Heute reicht das HTML-Attribut. Für Browser ohne Support gibt es Fallback-Verhalten (sofortiges Laden).
width/height am img — verhindert Layout-Shift (CLS).
Ohne width/height weiß der Browser die Bild-Größe erst beim Laden — Inhalt darunter springt. Mit width/height (auch als JSX-Props mit Pixel-Zahl) reserviert er den Platz vorab. Wichtig für Core Web Vitals.
JSX nutzt camelCase: srcSet, crossOrigin, useMap.
HTML-Attribute mit Bindestrich oder Klein-Schreibweise werden in JSX zu camelCase. srcSet, crossOrigin, useMap, referrerPolicy. Linter erkennt das.
public-Bilder haben kein Cache-Busting.
Ändert sich ein Bild in /public, behalten Browser oft die alte Version. Manuelle Cache-Busting: Query-Parameter mit Versions-Hash ?v=2026-05 oder das Bild umbenennen.
img-Tag MUSS in JSX selbst-schließend sein.
<img> ohne /> ist ein JSX-Syntax-Fehler. <img /> oder <img></img> sind beide gültig — selbst-schließend ist Konvention.
onError ohne Loop-Schutz kann endlos feuern.
Wenn der Fallback im onError selbst nicht lädt, feuert onError erneut. Lösung: in onError e.currentTarget.onerror = null setzen, BEVOR der Fallback-src zugewiesen wird.
Weiterführende Ressourcen
Externe Quellen
<img>– MDN- Lazy loading via attribute – MDN
- Responsive images – MDN
- Vite – Asset Handling
- Cumulative Layout Shift – web.dev