Files
Wabenhain/src/components/content-image.tsx
T
Nick Szaunig 107cbadf79 perf: Header-Medien sofort und in passender Groesse laden
Drei Gruende, warum Hero-Bild und -Video spuerbar spaet erschienen:

1. Das Standbild des Hero-Videos steckte im poster-Attribut. poster nimmt
   nur eine fertige URL und laedt die hochgeladene Datei damit in voller
   gespeicherter Groesse — am Bildoptimierer vorbei, ohne Ruecksicht auf
   das Geraet. Es liegt jetzt als eigene ContentImage-Ebene unter dem
   Video, laeuft also durch next/image. Gemessen: auf einem 500px breiten
   Viewport kommt statt der vollen Datei die 640px-Variante als WebP an.
   Das Video darueber ist transparent, bis es eigene Bilddaten hat — der
   Uebergang bleibt unsichtbar.

2. Die aeusserste Hero-Section startete bei opacity 0 und wurde erst per
   Animation nach der Hydration sichtbar. Das groesste Element im ersten
   Viewport wurde damit um die gesamte JS-Ladezeit zurueckgehalten. Die
   Animation dort ist entfernt; der gestaffelte Aufbau der Textspalte
   bleibt unveraendert.

3. next/image erhielt weiterhin die Prop "priority". Die ist seit
   Next.js 16 abgekuendigt (siehe node_modules/next/dist/docs, Changelog
   v16.0.0) und erzeugt nur noch einen <link rel="preload">, setzt am
   <img> selbst aber weder fetchpriority noch loading — im Trace kam das
   Hero-Bild deshalb trotz "priority" mit Prioritaet "Low" an. Die
   Dokumentation nennt dafuer ausdruecklich loading="eager" und
   fetchPriority="high" als Ersatz; ContentImage bildet seine unveraendert
   heissende priority-Prop jetzt darauf ab, die Produktgalerie ebenso.
   Danach bestand die LCP-Discovery-Pruefung von Chrome vollstaendig,
   vorher scheiterte sie an fetchpriority.

Ausserdem bekommen Inhaltsbilder leicht abgerundete Ecken (8px), Platzhalter
und echtes Bild gleichermassen, damit die Form beim Pflegen eines Bildes
nicht umspringt. Randlose Flaechen (Hero ueber die volle Fensterbreite)
bleiben bewusst eckig — dort saesse die Rundung an der Fensterkante.

Geprueft im Produktions-Build gegen Chrome: CLS 0.00, kein horizontaler
Ueberlauf bei schmalem Viewport, Video spielt (readyState 4), Hero-Bild
loading="eager" + fetchpriority="high", uebrige Bilder lazy.
2026-09-14 20:36:53 +02:00

80 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Image from "next/image";
import { PlaceholderImage } from "@/components/placeholder-image";
interface ContentImageProps {
/** Hochgeladener Bildpfad (z.B. "/uploads/<id>.webp") oder null/leer -> Platzhalter. */
src: string | null | undefined;
alt: string;
/** Groessen-/Seitenverhaeltnis-Klassen (z.B. "aspect-[4/5] w-full") — gilt fuer Platzhalter UND Bild-Wrapper gleichermassen. */
className: string;
tone?: "forest" | "cream" | "honey";
label?: string;
sizes?: string;
/** Fuer das wahrscheinlichste LCP-Bild einer Seite (z.B. Startseiten-Hero)
* true setzen — laedt das Bild mit hoher Prioritaet ohne Lazy-Loading,
* statt es erst beim Scannen weiter unten im Dokument zu entdecken. */
priority?: boolean;
/** Leicht abgerundete Ecken. Standard true — passt fuer Karten, Teaser
* und Inhaltsbilder, die als eigene Flaeche auf dem Seitenhintergrund
* liegen. Fuer randlose Flaechen (Hero ueber die volle Fensterbreite)
* bewusst false: dort saesse die Rundung direkt an der Fensterkante und
* wirkte wie ein Darstellungsfehler. */
rounded?: boolean;
}
/**
* Zeigt ein hochgeladenes Bild an, sobald eines gepflegt ist, sonst denselben
* PlaceholderImage-Platzhalter wie zuvor an dieser Stelle — nie ein kaputtes
* <img>. Uploads liegen same-origin unter /uploads/... (siehe
* app/uploads/[...path]/route.ts) und brauchen deshalb KEINEN
* next.config.ts-remotePatterns-Eintrag: remotePatterns/domains werden nur
* fuer externe (cross-origin) Bildquellen benoetigt, next/image behandelt
* einen mit "/" beginnenden src immer als lokal und ruft dafuer intern die
* eigene Optimierungsroute auf.
*/
export function ContentImage({
src,
alt,
className,
tone = "forest",
label,
sizes = "100vw",
priority = false,
rounded = true,
}: ContentImageProps) {
// Dieselbe Rundung fuer Platzhalter UND echtes Bild — sonst spraenge die
// Form in dem Moment um, in dem ein Bild gepflegt wird.
const roundedClass = rounded ? "rounded-lg" : "";
if (!src) {
return (
<PlaceholderImage tone={tone} className={`${roundedClass} ${className}`} label={label} />
);
}
// "label" traegt bereits eine kurze inhaltliche Einordnung (Ernte-Jahr/Typ,
// Kategorie, Ort — siehe Aufrufer), die bisher nur auf der Platzhalterkachel
// sichtbar war und bei echten Fotos verworfen wurde. Fuer echte Bilder in
// den Alt-Text uebernommen: aussagekraeftiger fuer Screenreader UND
// Bildersuche als der Produkt-/Ernteame allein.
const descriptiveAlt = label ? `${alt} – ${label}` : alt;
return (
<div className={`relative overflow-hidden ${roundedClass} ${className}`}>
<Image
src={src}
alt={descriptiveAlt}
fill
sizes={sizes}
// Next.js 16 hat die frueher hier verwendete Prop "priority"
// abgekuendigt: sie erzeugt nur noch einen <link rel="preload">, setzt
// am <img> selbst aber weder fetchpriority noch loading. Gemessen kam
// das Hero-Bild damit trotz "priority" mit niedriger Netzwerk-
// Prioritaet an. Die Dokumentation nennt fuer diesen Fall
// ausdruecklich loading="eager" und fetchPriority="high" als Ersatz.
loading={priority ? "eager" : "lazy"}
fetchPriority={priority ? "high" : "auto"}
className="object-cover"
/>
</div>
);
}