Zum Inhalt springen

Bildoptimierung für Web-Performance

Praktischer Leitfaden zur Bildoptimierung im Web: moderne Formate, responsive Bilder, Lazy Loading, CDN-Auslieferung und Core-Web-Vitals-Wirkung.

5 Min. Lesezeit
Vorher-Nachher-Vergleich, der die Reduzierung der Bilddateigröße von 2 MB auf 40 KB bei erhaltener visueller Qualität zeigt

Bilder machen auf den meisten Webseiten den Großteil der übertragenen Bytes aus. Ein unoptimiertes Hero-Bild kann im Alleingang den Largest-Contentful-Paint-Wert ruinieren. Die gute Nachricht: Bildoptimierung ist eine der wirkungsvollsten und zugleich am wenigsten aufwendigen Performance-Verbesserungen, die sich umsetzen lassen. Moderne Formate, responsive Größen und Lazy Loading können die Bildlast um 70–90 % reduzieren, ohne dass ein sichtbarer Qualitätsverlust entsteht.

Das Ziel ist nicht, Bilder so klein wie möglich zu machen, sondern das richtige Bild in der richtigen Größe, im richtigen Format und zum richtigen Zeitpunkt auszuliefern.

Moderne Bildformate

WebP und AVIF bieten eine deutlich bessere Kompression als JPEG und PNG, ohne dabei an visueller Qualität einzubüßen. Die Browserunterstützung ist inzwischen breit genug, um sie als primäres Format einzusetzen.

htmlhtml
<!-- ❌ Serving a single JPEG format to all browsers -->
<img src="/photos/hero.jpg" alt="Product showcase" />
<!-- 2.1 MB — unoptimized, one size for all screens -->
 
<!-- ✅ Serving modern formats with fallback -->
<picture>
  <!-- AVIF: smallest file size, 94% browser support (2024) -->
  <source srcset="/photos/hero.avif" type="image/avif" />
  <!-- WebP: good compression, 97% browser support -->
  <source srcset="/photos/hero.webp" type="image/webp" />
  <!-- JPEG: universal fallback -->
  <img src="/photos/hero.jpg" alt="Product showcase"
       width="1200" height="600"
       loading="eager"
       fetchpriority="high" />
</picture>
<!-- AVIF: ~180 KB, WebP: ~280 KB, JPEG: ~2.1 MB -->
<!-- Same visual quality, 87% smaller with AVIF -->
shbash
# Convert images to modern formats using sharp-cli
# Install: npm install -g sharp-cli
 
# JPEG → WebP (quality 80 is visually lossless for photos)
sharp -i hero.jpg -o hero.webp --format webp --quality 80
 
# JPEG → AVIF (quality 60 is comparable to WebP quality 80)
sharp -i hero.jpg -o hero.avif --format avif --quality 60
 
# PNG → WebP (for images with transparency)
sharp -i logo.png -o logo.webp --format webp --quality 90
 
# Batch convert all JPEGs in a directory
for f in *.jpg; do
  sharp -i "$f" -o "${f%.jpg}.webp" --format webp --quality 80
  sharp -i "$f" -o "${f%.jpg}.avif" --format avif --quality 60
done

Responsive Bilder mit srcset

Ein 2000 Pixel breites Bild an einen 400 Pixel breiten Mobilbildschirm auszuliefern, verschwendet Bandbreite. Responsive Bilder lassen den Browser die passende Größe anhand von Bildschirmbreite und Pixeldichte auswählen.

htmlhtml
<!-- ❌ One image size for all devices -->
<img src="/photos/hero-2000w.jpg" alt="Product showcase" />
<!-- Mobile downloads 2000px image, displays at 400px — wasteful -->
 
<!-- ✅ Multiple sizes — browser picks the best one -->
<img
  srcset="
    /photos/hero-400w.webp   400w,
    /photos/hero-800w.webp   800w,
    /photos/hero-1200w.webp 1200w,
    /photos/hero-2000w.webp 2000w
  "
  sizes="
    (max-width: 640px) 100vw,
    (max-width: 1024px) 80vw,
    60vw
  "
  src="/photos/hero-1200w.webp"
  alt="Product showcase"
  width="1200"
  height="600"
/>
<!-- Mobile downloads 400w (50KB), desktop downloads 2000w (280KB) -->
tstypescript
// Build script to generate responsive image variants
import sharp from 'sharp';
import { readdir } from 'fs/promises';
import { join, parse } from 'path';
 
const WIDTHS = [400, 800, 1200, 2000];
const FORMATS: Array<{ ext: string; options: object }> = [
  { ext: 'avif', options: { quality: 60 } },
  { ext: 'webp', options: { quality: 80 } },
];
 
async function generateResponsiveImages(inputDir: string, outputDir: string) {
  const files = await readdir(inputDir);
 
  for (const file of files) {
    const { name } = parse(file);
    const inputPath = join(inputDir, file);
 
    for (const width of WIDTHS) {
      for (const format of FORMATS) {
        const outputPath = join(outputDir, `${name}-${width}w.${format.ext}`);
 
        await sharp(inputPath)
          .resize(width, null, { withoutEnlargement: true })
          .toFormat(format.ext as keyof sharp.FormatEnum, format.options)
          .toFile(outputPath);
 
        console.log(`Generated: ${outputPath}`);
      }
    }
  }
}
 
generateResponsiveImages('./originals', './public/images');

Lazy Loading und Prioritätshinweise

Nicht alle Bilder müssen sofort geladen werden. Bilder oberhalb des sichtbaren Bereichs brauchen ein sofortiges Laden mit hoher Priorität. Bilder weiter unten auf der Seite sollten per Lazy Loading nachgeladen werden – der Browser ruft sie erst ab, wenn sie sich dem Viewport nähern.

htmlhtml
<!-- Above the fold: load immediately with high priority -->
<img
  src="/photos/hero.webp"
  alt="Hero banner"
  width="1200" height="600"
  loading="eager"
  fetchpriority="high"
  decoding="async"
/>
 
<!-- Below the fold: lazy load when approaching viewport -->
<img
  src="/photos/feature-1.webp"
  alt="Feature showcase"
  width="600" height="400"
  loading="lazy"
  decoding="async"
/>
tstypescript
// ❌ Lazy loading ALL images — including the hero
// The hero image is the Largest Contentful Paint element
// Lazy loading it DELAYS LCP, making performance worse
<img src="/hero.webp" loading="lazy" /> // Don't do this
 
// ✅ Eager load above-the-fold, lazy load everything else
// Rule of thumb: the first 1-2 images should be eager
// Everything below the initial viewport should be lazy
htmlhtml
<!-- Preload critical hero image for fastest LCP -->
<head>
  <link
    rel="preload"
    as="image"
    href="/photos/hero.avif"
    type="image/avif"
    fetchpriority="high"
  />
</head>
<!-- This tells the browser to fetch the hero image immediately,
     before it even parses the <img> tag in the body -->

Layout-Verschiebungen vermeiden

Bilder ohne explizite Abmessungen verursachen Layout-Verschiebungen: Der Inhalt springt, während die Bilder geladen werden. Das ruiniert den CLS-Wert (Cumulative Layout Shift).

htmlhtml
<!-- ❌ No dimensions — causes layout shift as image loads -->
<img src="/photos/product.webp" alt="Product" />
<!-- Browser doesn't know the size until image loads → content jumps -->
 
<!-- ✅ Explicit width and height — browser reserves space -->
<img
  src="/photos/product.webp"
  alt="Product"
  width="600"
  height="400"
  style="max-width: 100%; height: auto;"
/>
<!-- Browser calculates aspect ratio from width/height -->
<!-- Reserves exact space before image loads → zero layout shift -->
csscss
/* Modern approach: aspect-ratio property */
.product-image {
  width: 100%;
  aspect-ratio: 3 / 2;  /* Maintains ratio without height attribute */
  object-fit: cover;     /* Fills container without distortion */
}
 
/* Placeholder blur while loading */
.image-container {
  position: relative;
  background-color: #e2e8f0;
  background-image: url('data:image/svg+xml,...'); /* Tiny blur placeholder */
  background-size: cover;
}
 
.image-container img {
  width: 100%;
  height: auto;
  transition: opacity 0.3s;
}
 
.image-container img[loading] {
  opacity: 0;
}

Wirkung mit Core Web Vitals messen

Bildoptimierung wirkt sich direkt auf zwei Core Web Vitals aus: LCP (Largest Contentful Paint) und CLS (Cumulative Layout Shift). Miss vorher und nachher, um die Verbesserung zu quantifizieren.

tstypescript
// Measure LCP in the browser
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    console.log('LCP:', entry.startTime, 'ms');
    console.log('Element:', (entry as any).element?.tagName);
    // If this is an <img>, your image optimization directly affects this number
  }
}).observe({ type: 'largest-contentful-paint', buffered: true });
 
// Measure CLS contributions
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (!(entry as any).hadRecentInput) {
      console.log('Layout shift:', (entry as any).value);
      for (const source of (entry as any).sources ?? []) {
        console.log('Shifted element:', source.node?.tagName);
        // Images without width/height appear here
      }
    }
  }
}).observe({ type: 'layout-shift', buffered: true });
ymlyaml
# Before and after optimization — real numbers
before_optimization:
  hero_image_size: "2.1 MB (JPEG, 3000x1500)"
  total_image_payload: "8.4 MB"
  lcp: "4.2 seconds"
  cls: "0.28"
 
after_optimization:
  hero_image_size: "180 KB (AVIF, responsive srcset)"
  total_image_payload: "1.2 MB"
  lcp: "1.8 seconds"      # 57% improvement
  cls: "0.02"              # 93% improvement
 
changes_made:
  - "Converted to AVIF/WebP with picture element"
  - "Generated 4 responsive sizes per image"
  - "Added width/height to all img tags"
  - "Lazy loaded below-fold images"
  - "Preloaded hero image in document head"

Die wichtigsten Erkenntnisse

  1. AVIF als primäres Format nutzen, WebP als Fallback — AVIF erzeugt bis zu 50 % kleinere Dateien als JPEG bei vergleichbarer Qualität und wird von 94 % der Browser unterstützt
  2. Responsive Bildvarianten erzeugen in 400w, 800w, 1200w und 2000w — der Browser wählt dann anhand der Bildschirmgröße
  3. Bilder oberhalb des sichtbaren Bereichs sofort laden, den Rest per Lazy Loading — und das LCP-Bild im head des Dokuments vorladen
  4. Immer die Attribute width und height setzen, um Layout-Verschiebungen zu vermeiden und einen CLS-Wert von null zu erhalten
  5. LCP und CLS vorher und nachher messen — Bildoptimierung gehört zu den wirkungsvollsten Performance-Verbesserungen überhaupt
  6. Mit einer Build-Pipeline automatisieren — sharp oder ähnliche Tools erzeugen alle Formate und Größen aus den Originalbildern
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX