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.
This commit is contained in:
Nick Szaunig committed 2026-09-14 20:36:53 +02:00
1 parent 2a16b3f962
commit 107cbadf79
3 files changed
+74 -21

No files matched your search

+9 -5
View File
@@ -63,7 +63,7 @@ export function ProductGallery({ images, productName, description }: ProductGall
return (
<div>
<div
className="relative aspect-[4/3] overflow-hidden touch-pan-y"
className="relative aspect-[4/3] overflow-hidden rounded-lg touch-pan-y"
onTouchStart={handleTouchStart}
onTouchEnd={handleTouchEnd}
>
@@ -72,14 +72,18 @@ export function ProductGallery({ images, productName, description }: ProductGall
// /uploads/... (siehe uploads.ts/isValidUploadPath) — next/image
// braucht dafuer KEINEN remotePatterns-Eintrag (nur externe
// Bildquellen brauchen den), siehe auch content-image.tsx. Als
// wahrscheinlichstes LCP-Element der Produktseite mit "priority"
// geladen, statt erst beim Scannen des Bild-Tags entdeckt zu werden.
// wahrscheinlichstes LCP-Element der Produktseite sofort und mit
// hoher Netzwerk-Prioritaet geladen, statt erst beim Scannen des
// Bild-Tags entdeckt zu werden. Bewusst NICHT ueber die Prop
// "priority": die ist seit Next.js 16 abgekuendigt und erzeugt nur
// noch einen Preload-Link, ohne fetchpriority am <img> zu setzen.
<Image
src={active.src}
alt={mainImageAlt}
fill
sizes="(min-width: 768px) 50vw, 100vw"
priority
loading="eager"
fetchPriority="high"
className="object-cover"
/>
) : (
@@ -95,7 +99,7 @@ export function ProductGallery({ images, productName, description }: ProductGall
onClick={() => setActiveIndex(index)}
aria-label={`${isImageSlide(slide) ? `Produktbild ${index + 1}` : slide.label} – Bild ${index + 1} von ${slides.length} anzeigen`}
aria-current={index === activeIndex}
className={`relative aspect-square overflow-hidden border-2 transition-colors ${
className={`relative aspect-square overflow-hidden rounded-lg border-2 transition-colors ${
index === activeIndex ? "border-forest" : "border-transparent hover:border-ink/20"
}`}
>