Zum Inhalt springen
StartseiteStart
EN
WhatsAppMailPhone
← Alle Artikel
Nachladen erst nach dem LCP, gesteuert über den PerformanceObserver
Performance & SEO

Nachladen erst nach dem LCP, gesteuert über den PerformanceObserver

Foto: veri_ivanova / Unsplash

Warum loading=lazy, requestIdleCallback und window.load dekorative Layer nicht zuverlässig aus der LCP-Kette halten und wie ein Gate am largest-contentful-paint-Eintrag das löst.

Eric MengeAutorEric MengeInhaber & Webentwickler bei EMIT Solution
Veröffentlicht
Lesezeitca. 8 Min.

Kurz gesagt

  • Lighthouse und PageSpeed Insights messen per Lantern-Simulation. Alles, was vor dem beobachteten LCP startet, geht als LCP-Abhängigkeit in die Rechnung ein, auch wenn der Paint es gar nicht braucht.
  • loading="lazy" ist ein Entfernungs-Gate, kein Zeit-Gate. Chromium lädt lazy markierte Bilder ab 1250 px Abstand zum Viewport bei 4G, und Elemente im Viewport laden unabhängig vom Attribut. opacity: 0 verhindert das Laden nicht, display: none schon.
  • Der Browser feuert mehrere largest-contentful-paint-Einträge pro Seitenaufruf und meldet nie, welcher der letzte war. Ein Gate braucht deshalb einen Settle-Timer nach dem jeweils letzten Eintrag plus einen harten Fallback.
  • observe() wirft bei einem nicht unterstützten Eintragstyp keine Exception, sondern bricht still ab. Ohne Prüfung über PerformanceObserver.supportedEntryTypes hängt das Gate auf Browsern ohne LCP-Support im Fallback-Timer fest.

Der PageSpeed-Report zeigt ein LCP von über sechs Sekunden. Der Filmstrip im selben Report zeigt eine fertig gemalte Seite nach gut einer Sekunde. Beides stimmt, und genau dieser Widerspruch ist der Grund, warum dekorative Layer, Frame-Sequenzen und Hintergrundvideos so oft die Messung ruinieren, obwohl sie optisch überhaupt keine Rolle spielen.

Bei der V3-Optimierung von emit-solution.com am 09.07.2026 war das der Kern des Problems. Der PageSpeed-Wert stieg am Ende von 70 auf 92 bis 96, das LCP fiel von 6,7 auf 2,4 Sekunden. Das sind Laborwerte aus der Simulation, keine Felddaten. Ein zentraler Baustein war ein Gate, das Nachlade-Arbeit erst nach dem letzten LCP-Kandidaten startet.

Warum Nachlade-Arbeit überhaupt im LCP landet

Lighthouse nutzt standardmäßig simuliertes Throttling, PageSpeed Insights ebenso. Die Seite wird auf dem Test-Runner ungedrosselt geladen, danach rechnet das Lantern-Modell die Metriken auf ein Mobilprofil hoch. Das Preset arbeitet mit 1638 Kbps Download, 750 Kbps Upload, 150 ms RTT und vierfacher CPU-Verlangsamung. Die Lighthouse-Doku räumt selbst eine “inherent inaccuracy” ein, weil alternative Ausführungspfade vorhergesagt werden müssen.

Die entscheidende Falle steckt in der Kettenbildung. Lantern zählt alle Requests, die vor dem beobachteten LCP starten, als LCP-Abhängigkeit, unabhängig davon, ob der Paint sie braucht. Eine Seite, die real in 1,5 Sekunden malt, parallel aber 300 Bilder und Frames anstößt, bekommt ein simuliertes LCP von über sechs Sekunden. Genauso gehen CPU-Tasks vor dem Paint mit Faktor vier in die Kette ein, synchrone Inline-Skripte und Hydration also, und tauchen im LCP-Breakdown als Render Delay auf.

Daraus folgt die eigentliche Aufgabe. Es geht nicht darum, weniger zu laden. Es geht darum, später zu starten, und zwar nachweislich später als der letzte LCP-Kandidat.

Hand hält ein Smartphone, auf dem dunklen Display ein großer grüner Kreis, im Hintergrund eine unscharfe Straßenszene Foto: kommumikation / Unsplash

loading=“lazy” ist ein Entfernungs-Gate

Das erste Mittel, zu dem die meisten greifen, misst gar keine Zeit. Chromium lädt lazy markierte Bilder ab 1250 px Abstand zum Viewport bei 4G und ab 2500 px bei 3G oder langsamer. Bilder, die ohne Scrollen sichtbar wären, laden regulär, unabhängig vom loading-Attribut.

Für dekorative Vollbild-Layer ist das genau der falsche Fall. Sie liegen im Viewport. Und der naheliegende Trick hilft nicht: opacity: 0 verhindert das Laden in Chrome, Safari und Firefox nicht, das Bild wird trotzdem geholt. Nur display: none unterdrückt den Request. Die Chrome-Empfehlung ist entsprechend deutlich formuliert, nämlich Bilder nicht lazy zu laden, die beim Seitenaufbau wahrscheinlich im Viewport liegen, LCP-Bilder erst recht nicht.

Der Fehler ist verbreitet genug, dass Lighthouse einen eigenen Audit dafür hat, lcp-lazy-loaded. Der Web Almanac 2025 beziffert rund 16 bis 17 Prozent aller Seiten, die ihr LCP-Bild lazy laden, unverändert gegenüber 2024. Aufgeschlüsselt sind das 10,4 Prozent mobil und 11,5 Prozent Desktop über natives loading=“lazy”, plus 5,9 Prozent mobil über eigene data-src-Lösungen. Zum Vergleich liegt fetchpriority=“high” bei 16,3 Prozent Desktop und 17,3 Prozent mobil, preload dagegen nur bei 2,1 bis 2,2 Prozent.

requestIdleCallback und window.load sind Münzwürfe

requestIdleCallback klingt nach der richtigen Semantik und ist trotzdem keine belastbare Bremse. MDN führt die API als “Limited availability”, caniuse nennt rund 80,95 Prozent globale Unterstützung. Für Safari sind die Angaben uneinheitlich. caniuse listet Desktop- und iOS-Safari bis 26.3 als nicht unterstützt und führt die Versionen 13.1 bis 26.5 zugleich als standardmäßig deaktiviert, aktiv nur in der Technology Preview. Ein belastbares Ausliefer-Datum lässt sich daraus nicht ableiten, die Tracking-Issue bei WebKit ist Bug 164193. Dazu kommt die Semantik selbst. Callbacks laufen in Leerlaufphasen am Frame-Ende oder bei Nutzer-Inaktivität, und MDN warnt ausdrücklich, dass sie ohne gesetztes timeout unter Umständen mehrere Sekunden ausbleiben. In der eigenen V3-Optimierung fiel mir zusätzlich auf, dass der Callback auf einer schnellen Maschine praktisch sofort lief. Das ist eine einzelne Beobachtung und keine Regel, in der Summe mit der dokumentierten Verzögerung nach oben bleibt aber nichts, worauf man ein Gate stellen kann.

Bei window.load ist die Lage etwas anders, als man landläufig liest. Dass load generell vor dem LCP feuert, ist keine allgemeine Regel und lässt sich so nicht belegen. Belegbar ist der Mechanismus dahinter: Lighthouse und PSI messen auf einem ungedrosselten Runner, und load liegt dort nah genug am beobachteten Paint, dass die anschließend gestartete Arbeit noch in die simulierte Kette fällt. Aus der eigenen V3-Optimierung ist dazu eine Zahl dokumentiert. Identischer Code lieferte einmal 2,1 Sekunden und einmal 6,2 Sekunden simuliertes LCP. Das belegt die Instabilität, nicht mehr, aber das reicht als Urteil. Auf sowas baut man kein Deployment.

Verknüpfungssymbole von Microsoft Edge, Firefox, Google Chrome, Opera und Brave nebeneinander auf einem violetten Desktop-Hintergrund Foto: redaquamedia / Unsplash

Wie der LCP-Eintrag wirklich funktioniert

Der Eintragstyp heißt exakt largest-contentful-paint, so definiert es die W3C-Spezifikation in Abschnitt 3.1. Drei Eigenschaften bestimmen, wie ein Gate aussehen muss.

Erstens feuert der Browser mehrere Einträge pro Seitenaufruf. Einen, sobald der erste Frame gemalt ist, und danach jedes Mal neu, wenn sich das größte contentful Element ändert. Die Spec formuliert das in Abschnitt 1.1 so, dass für jeden neu gefundenen größten Inhalt ein neuer Eintrag erzeugt wird.

Zweitens gibt es kein Signal, welcher Eintrag der finale ist. Google empfiehlt deshalb ausdrücklich, nur den zuletzt gefeuerten Eintrag zu melden. Die offizielle web-vitals-Bibliothek arbeitet genau so, mit gesetztem buffered-Flag, und ob reportAllChanges gesetzt ist, ändert den final gemeldeten Wert nicht.

Drittens endet die Suche bei Nutzerinteraktion. Abschnitt 4.2 der Spec bricht das Reporting ab, wenn das Fenster ein scroll- oder input-Event ausgeliefert hat. Genau das lässt sich als Abkürzung nutzen, denn wenn der Nutzer scrollt, braucht er den Inhalt ohnehin sofort.

Das kanonische Auswertungsmuster sieht so aus, mit type statt entryTypes, weil buffered nur in dieser Kombination erlaubt ist:

const observer = new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const lastEntry = entries[entries.length - 1];
  console.log('LCP:', lastEntry.startTime, lastEntry.element);
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });

Das Gate

Die Fassung, auf die es hinausläuft, kombiniert vier Bausteine. Ein Settle-Timer startet nach jedem LCP-Eintrag neu, ein harter Fallback-Timer greift, falls gar kein Eintrag kommt, eine Interaktions-Abkürzung feuert sofort, und eine Feature-Detection entscheidet vorab, ob überhaupt auf Einträge gewartet werden kann.

Der letzte Punkt ist wichtig und in der ursprünglichen Fassung falsch gelöst. Ein try/catch um observe() greift nicht, weil observe() bei einem nicht unterstützten Typ still abbricht und keine Exception wirft. Die Performance Timeline Spec sagt in Schritt 7.2 des Algorithmus, dass die Schritte abgebrochen werden, wenn der Typ nicht im eingefrorenen Array der unterstützten Eintragstypen enthalten ist. Auf einem Safari vor 26.2 landet der Code deshalb nie im arm()-Zweig, sondern verharrt volle fünf Sekunden im Fallback-Timer. Korrekt geprüft wird über PerformanceObserver.supportedEntryTypes:

function afterLcp(fn, settleMs) {
  var done = false, po = null;
  var settle = Math.max(settleMs || 0, 1600);
  var supported = typeof PerformanceObserver !== 'undefined' &&
    PerformanceObserver.supportedEntryTypes &&
    PerformanceObserver.supportedEntryTypes.indexOf('largest-contentful-paint') !== -1;

  // ohne LCP-Support direkt in den kurzen Timer, nicht in den 5s-Fallback
  var timer = setTimeout(fire, supported ? 5000 : settle);

  function fire() {
    if (done) return;
    done = true;
    clearTimeout(timer);
    try { po && po.disconnect(); } catch (e) {}
    fn();
  }
  function arm() {                       // nach jedem LCP-Eintrag neu aufziehen
    clearTimeout(timer);
    timer = setTimeout(fire, settle);
  }
  if (supported) {
    po = new PerformanceObserver(arm);
    po.observe({ type: 'largest-contentful-paint', buffered: true });
  }
  ['pointerdown', 'wheel', 'touchstart', 'keydown'].forEach(function (ev) {
    window.addEventListener(ev, fire, { once: true, passive: true });
  });
}

Die 1600 ms Settle-Zeit sind ein Erfahrungswert aus der eigenen Optimierung und nicht aus der Spec ableitbar. Wer kürzer geht, riskiert, dass ein später Font-Swap oder ein nachrutschendes Hero-Element noch einen Eintrag erzeugt, nachdem das Gate schon gefeuert hat.

Am Gate hängen in der eigenen Umsetzung Frame-Sequenzen, Videos, dekorative Vollbild-Layer und spekulative Font-Loads. Die Markup-Seite sieht dafür so aus, mit data-src statt src und einem Video ohne Vorabladen:

<img class="decor" data-src="/layer.webp" alt="" decoding="async">
<video id="bg" preload="none" muted playsinline></video>

Und die Anwendung im Gate:

afterLcp(function () {
  document.querySelectorAll('img[data-src]').forEach(function (el) { el.src = el.dataset.src; });
  document.querySelectorAll('[data-bg]').forEach(function (el) { el.style.backgroundImage = 'url(' + el.dataset.bg + ')'; });
  var v = document.getElementById('bg'); if (v) { v.src = '/bg.mp4'; v.load(); }
}, 1600);

Was das Gate kippen kann

Vier Punkte gehören auf die Prüfliste, bevor man sich auf das Muster verlässt.

Elemente mit opacity: 0 werden von den Chromium-Heuristiken als LCP-Kandidat ausgeschlossen, weil sie für den Nutzer unsichtbar sind. Ein transparent gehaltener Layer verschiebt das LCP also nicht, lädt aber trotzdem, siehe oben.

Seit Chrome 112 ignoriert LCP außerdem Bilder mit weniger als 0,05 Bit Bilddaten pro dargestelltem Pixel. Der Rollout begann etwa am 6. April 2023 und wirkt rückwirkend ab Chrome 109. Das trifft vor allem große einfache Hintergrundbilder, Viewport-Overlays und Platzhalter. Beides sind Chromium-Heuristiken, keine Spec-Anforderungen, in anderen Engines kann das abweichen.

Alte Taschenuhr mit weissem Zifferblatt in einer Hand Foto: thilak_cm212 / Unsplash

Nach einer Wiederherstellung aus dem Back/Forward-Cache meldet die API gar keine largest-contentful-paint-Einträge mehr. In dem Fall trägt ausschließlich der Fallback-Timer, was ein weiteres Argument dafür ist, ihn nicht zu großzügig zu setzen.

Nicht geprüft habe ich das Verhalten bei clientseitiger Navigation über die View Transitions API. Weil LCP-Einträge dokumentweise entstehen, ist zu erwarten, dass eine zweite Navigation im selben Dokument keine neuen Einträge mehr liefert und das Gate dort anders angebunden werden müsste. Nachgemessen habe ich das nicht. Ebenso offen ist, ob programmatisches Scrollen die LCP-Suche genauso beendet wie eine echte Nutzereingabe.

Ergebnis prüfen statt glauben

Der erste Diagnoseschritt ist immer der Vergleich von beobachtetem und simuliertem LCP:

npx lighthouse <url> --form-factor=mobile --screenEmulation.mobile --throttling-method=simulate \
  --only-categories=performance --output=json --output-path=audit.json --chrome-flags="--headless=new"

Im JSON steht unter audits.metrics der Wert observedLargestContentfulPaint, also die echte Paint-Zeit. Ist der beobachtete FCP gleich dem beobachteten LCP und beides schnell, die Simulation aber langsam, liegt ein Ketten-Problem vor und kein Render-Problem. Ergänzend lohnt audits['largest-contentful-paint-element'] und audits['network-requests'], gefiltert auf networkRequestTime kleiner als das beobachtete LCP. Das ist die tatsächliche Kette.

Einzelläufe sind wertlos. In der eigenen Optimierung wurde identischer Code einmal mit 52 und einmal mit 95 Punkten gemessen, weil PSI-Runner stark streuen. Drei Läufe, Median. Ein TBT über 0,5 Sekunden bei sonst gleichen Werten deutet auf einen lahmen Runner hin, nicht auf die Seite.

Bildschirm mit Diagrammen zu Ladezeiten und Kennzahlen Foto: justin_morgan / Unsplash

Und das Gate ist selten der einzige Hebel. Render-blocking CSS war im eigenen Fall der verdeckende Faktor, 42 KB externes CSS kosteten allein rund 3,5 Sekunden simuliertes LCP, gelöst über build: { inlineStylesheets: 'always' } in Astro. Synchrone Body-Inline-Skripte in eine Defer-Queue zu verschieben brachte den beobachteten Paint von 1,34 auf 1,07 Sekunden. Und die eager geladenen Webfonts wurden von 13 auf 3 reduziert. Wer tiefer in die Metrik-Gewichtung einsteigen will, findet das im Leitfaden zum Lighthouse-Score.

Zuletzt die Einordnung, die man sich sparen kann zu vergessen. Google rankt nach CrUX-Felddaten, nicht nach dem Laborwert. Ob ein LCP-Gate die Feldwerte verbessert oder vor allem die Simulation, ist nicht belegt. Der Laborwert bleibt trotzdem nützlich, weil er die Kette sichtbar macht, die reale Nutzer auf schlechtem Netz genauso trifft.

Wenn dein PageSpeed-Bericht ein langsames LCP zeigt, der Filmstrip aber eine früh fertige Seite, dann steckt das Problem fast immer in dieser Kette und nicht im Rendering. Schreib mir kurz, mit welchem Wert du dastehst und was auf der Seite alles nachlädt, dann schaue ich mir den Lighthouse-Report an und sage dir, ob ein Gate der richtige Hebel ist.

Häufige Fragen

Warum lädt ein Bild mit loading="lazy" trotzdem sofort?+

Weil loading="lazy" den Abstand zum Viewport bewertet und nicht den Zeitpunkt. Chromium lädt lazy markierte Bilder ab rund 1250 px Entfernung zum Viewport bei 4G und ab 2500 px bei 3G oder langsamer. Alles, was ohne Scrollen sichtbar wäre, lädt regulär. Auch ein Element mit opacity: 0 zählt als sichtbar im Sinne des Ladens, nur display: none verhindert den Request in Chrome, Safari und Firefox.

Wie erkenne ich den finalen LCP-Eintrag?+

Gar nicht direkt. Die Spezifikation sieht vor, dass für jedes neue größte Inhaltselement ein neuer Eintrag erzeugt wird, und es gibt kein Signal für den letzten. Google empfiehlt deshalb, immer nur den zuletzt gefeuerten Eintrag auszuwerten. Praktisch arbeitet man mit einem Settle-Timer, der nach jedem neuen Eintrag neu startet.

Beendet Scrollen die LCP-Messung?+

Ja. Die Spezifikation bricht das Reporting ab, sobald das Fenster ein scroll- oder input-Event ausgeliefert hat. web.dev formuliert dasselbe praxisnah: Der Browser hört auf, neue Einträge zu erzeugen, sobald der Nutzer per Tap, Scroll oder Tastendruck interagiert. Nach einer Wiederherstellung aus dem Back/Forward-Cache meldet die API überhaupt keine largest-contentful-paint-Einträge mehr.

Welche Browser unterstützen LargestContentfulPaint?+

Chrome ab Version 77, Edge ab 79, Firefox ab 122 sowie Safari und iOS Safari erst ab 26.2. caniuse weist rund 89,19 Prozent globale Unterstützung aus, MDN führt die API seit Dezember 2025 als Baseline. Für alle übrigen Browser braucht ein Gate einen eigenen Zeitpfad, sonst wartet es unnötig lange.

Du willst mehr erfahren?

In einem kostenlosen Erstgespräch besprechen wir, wie du diese Themen für dein Unternehmen nutzen kannst. Kein Verkaufsgespräch, sondern eine ehrliche Einschätzung.

Kostenloses Erstgespräch vereinbaren