Core Web Vitals: LCP, CLS und INP Optimierung für SaaS

Ein umfassender Leitfaden zur Optimierung der Core Web Vitals für SaaS-Anwendungen – Strategien für LCP, CLS, INP, Edge-Messung und Performance-Budgets.

Alex Chen
Alex Chen
16. Juni 202614 min read
Auch verfügbar auf:English · 中文

Zusammenfassung: Googles Core Web Vitals – LCP (Largest Contentful Paint), CLS (Cumulative Layout Shift) und INP (Interaction to Next Paint, welches FID 2024 ersetzt) – haben direkten Einfluss auf die Suchmaschinen-Rankings und Konversionsraten Ihres SaaS-Produkts. Dieser Leitfaden behandelt fortgeschrittene Optimierungstechniken für jede Metrik, Messstrategien auf Cloudflare Workers und eine Build-vs-Buy-Analyse zur Implementierung von Verbesserungen der Web Vitals.


Warum Core Web Vitals für SaaS wichtig sind

MetrikAuswirkungGoogle-GrenzwertAuswirkung auf SaaS-Konversion
LCPWahrgenommene Ladezeit< 2.5s1% Konversionsrückgang pro 100ms Verzögerung
CLSVisuelle Stabilität< 0.10.3% Konversionsrückgang pro 0.01 Anstieg
INPReaktionsfähigkeit< 200ms0.5% Konversionsrückgang pro 100ms Anstieg
TTFBServer-Antwort< 800msSEO-Ranking-Faktor

Ein SaaS, das seine CWV von „Verbesserung erforderlich“ auf „Gut“ steigert, verzeichnet typischerweise eine Steigerung des organischen Traffics um 5-15% und eine Verbesserung der Konversionsrate um 2-5%.


LCP: Largest Contentful Paint

LCP misst, wann das größte sichtbare Element (Hero-Bild, Überschrift oder Video) sichtbar wird. Zielwert: < 2.5 Sekunden.

LCP-Optimierung für SaaS

1. Kritische Ressourcen vorladen (Preload)

typescript
// In Ihrer TanStack Router Root-Route, LCP-Elemente vorladen
export const Route = createRootRoute({
  component: () => (
    <head>
      <link rel="preload" href="/hero.webp" as="image" />
      <link rel="preload" href="/fonts/inter-var.woff2" as="font" crossOrigin="anonymous" />
      <link rel="preconnect" href="https://api.tanstackship.com" />
    </head>
  ),
})

2. Serverseitiges Rendern von Above-the-Fold Inhalten

Das Streaming SSR von TanStack Start stellt sicher, dass das LCP-Element Teil des anfänglichen HTML ist:

typescript
export const Route = createFileRoute("/")({
  loader: async ({ context }) => {
    // Hero-Inhalte serverseitig abrufen — kein Client-Wasserfall
    return {
      heroContent: await context.env.DB.prepare(
        "SELECT heading, subheading, cta_text FROM hero_content WHERE active = 1"
      ).first(),
    }
  },
  component: HeroSection,
})

function HeroSection() {
  const { heroContent } = Route.useLoaderData()
  // heroContent rendern — er befindet sich bereits im HTML-Stream
}

3. Bilder optimieren

typescript
// Cloudflare Image Resizing für die automatische Optimierung nutzen
function HeroImage() {
  return (
    <img
      src="/cdn-cgi/image/width=1200,format=webp,quality=80/hero.jpg"
      alt="SaaS Dashboard"
      width={1200}
      height={600}
      loading="eager" // LCP-Bilder sollten nicht lazy geladen werden
      fetchPriority="high"
    />
  )
}

Auswirkungen auf den LCP-Score

OptimierungTypische VerbesserungAufwand
Hero-Bild vorladen200-400ms ReduzierungGering
Inhalte serverseitig rendern300-600ms ReduzierungMittel
Bildoptimierung (WebP/AVIF)200-800ms ReduzierungGering
Schriftarten vorladen100-300ms ReduzierungGering
CDN/Caching100-500ms ReduzierungGering
Render-blockierendes CSS reduzieren200-500ms ReduzierungMittel

CLS: Cumulative Layout Shift

CLS misst unerwartete Layout-Verschiebungen. Zielwert: < 0.1.

CLS-Lösungen für SaaS

1. Explizite Abmessungen für Medien festlegen

typescript
// Immer width und height bei Bildern und iFrames setzen
function SaaScreenshot() {
  return (
    <img
      src="/dashboard-preview.webp"
      alt="Dashboard preview"
      width={1200}
      height={675}
      style={{ aspectRatio: "16/9" }}
    />
  )
}

2. Platzhalter für dynamische Inhalte reservieren

typescript
// Platz für asynchron ladende Inhalte reservieren
.dynamic-content-placeholder {
  width: 100%;
  min-height: 200px;
  background: var(--skeleton-bg);
  border-radius: 8px;
}

function DynamicDashboard() {
  return (
    <Suspense fallback={<div className="dynamic-content-placeholder" />}>
      <DashboardContent />
    </Suspense>
  )
}

3. Layout-Verschiebungen durch Web-Schriftarten verhindern

typescript
// font-display: swap mit angepassten Fallback-Metriken verwenden
@font-face {
  font-family: "Inter";
  src: url("/fonts/inter-var.woff2") format("woff2");
  font-display: swap;
  size-adjust: 100%; /* Für konsistente Metriken anpassen */
}

4. Das Einfügen von Inhalten über bestehenden Inhalten vermeiden

typescript
// Schlecht: Ein Banner über bestehendem Inhalt einfügen
// document.body.prepend(banner) // ← verursacht Layout-Verschiebung

// Gut: Unterhalb des inhaltlichen Sichtbereichs (Below the Fold) einfügen
function CookieBanner() {
  const [visible, setVisible] = useState(true)

  useEffect(() => {
    // Anfängliches Layout messen, dann Banner anzeigen
    requestAnimationFrame(() => setVisible(true))
  }, [])

  if (!visible) return null

  return <div className="cookie-banner">...</div>
}

INP: Interaction to Next Paint (ersetzt FID)

INP misst die Zeit von einer Benutzerinteraktion (Klick, Tippen, Tastendruck) bis zur nächsten Frame-Aktualisierung. Zielwert: < 200ms.

INP-Optimierung

1. Lange Aufgaben aufteilen

typescript
// Vorher: Blockiert den Main Thread
function processAllRows(rows: Row[]) {
  rows.forEach((row) => expensiveOperation(row)) // blockiert die UI für 500ms
}

// Nachher: Übergabe an die Event Loop
async function processRowsProgressive(rows: Row[]) {
  const chunkSize = 50
  for (let i = 0; i < rows.length; i += chunkSize) {
    const chunk = rows.slice(i, i + chunkSize)
    chunk.forEach((row) => expensiveOperation(row))
    await new Promise((resolve) => setTimeout(resolve, 0)) // yield
  }
}

2. Nicht-kritisches JavaScript verzögern

typescript
// Schwere Komponenten verzögert laden (Lazy Load)
const AnalyticsChart = lazy(() => import("./AnalyticsChart"))
const ExportButton = lazy(() => import("./ExportButton"))

function Dashboard() {
  return (
    <Suspense fallback={<ChartSkeleton />}>
      <AnalyticsChart />
    </Suspense>
  )
}

3. isPending für Optimistic UI nutzen

typescript
const mutation = useMutation({
  mutationFn: updateEmail,
  onMutate: async (newEmail) => {
    // Optimistisches Update — UI reagiert sofort
    queryClient.setQueryData(["user"], (old) => ({ ...old, email: newEmail }))
  },
  onError: () => {
    // Rollback bei Fehlschlag
    queryClient.invalidateQueries({ queryKey: ["user"] })
  },
})

Web Vitals in Produktion messen

typescript
// src/lib/web-vitals.ts
import { onLCP, onCLS, onINP, onTTFB } from "web-vitals"

export function reportWebVitals() {
  onLCP((metric) => sendMetric("LCP", metric.value))
  onCLS((metric) => sendMetric("CLS", metric.value))
  onINP((metric) => sendMetric("INP", metric.value))
  onTTFB((metric) => sendMetric("TTFB", metric.value))
}

async function sendMetric(name: string, value: number) {
  // An Ihre Analytics-Engine senden
  await fetch("/api/vitals", {
    method: "POST",
    body: JSON.stringify({ name, value, url: window.location.pathname }),
  })
}
typescript
// Serverseitig — Web Vitals-Daten in D1 speichern
export const reportVital = createServerFn({ method: "POST" }).handler(
  async ({ data, context }) => {
    await context.env.DB.prepare(`
      INSERT INTO web_vitals (id, name, value, path, user_id, created_at)
      VALUES (?, ?, ?, ?, ?, ?)
    `).bind(
      crypto.randomUUID(),
      data.name,
      data.value,
      data.url,
      context.session?.userId ?? null,
      Date.now()
    ).run()
  }
)

Web Vitals Dashboard

typescript
export const getVitalsDashboard = createServerFn({ method: "GET" }).handler(
  async ({}, { context }) => {
    const lcp = await context.env.DB.prepare(`
      SELECT AVG(value) as avg, PERCENTILE(value, 75) as p75,
             PERCENTILE(value, 95) as p95
      FROM web_vitals
      WHERE name = 'LCP' AND created_at > datetime('now', '-7 days')
    `).first()

    const cls = await context.env.DB.prepare(`...`).first()
    const inp = await context.env.DB.prepare(`...`).first()

    const passingRate = await context.env.DB.prepare(`
      SELECT
        COUNT(*) as total,
        SUM(CASE WHEN name = 'LCP' AND value < 2500 THEN 1 ELSE 0 END) as good_lcp,
        SUM(CASE WHEN name = 'CLS' AND value < 0.1 THEN 1 ELSE 0 END) as good_cls,
        SUM(CASE WHEN name = 'INP' AND value < 200 THEN 1 ELSE 0 END) as good_inp
      FROM web_vitals
      WHERE created_at > datetime('now', '-7 days')
    `).first()

    return { lcp, cls, inp, passingRate }
  }
)

Einhaltung des Performance-Budgets

typescript
// CI-Check — sicherstellen, dass Web Vitals die Grenzwerte einhalten
const BUDGETS = {
  LCP: 2500,
  CLS: 0.1,
  INP: 200,
  TTFB: 800,
}

export const checkPerformanceBudget = createServerFn({ method: "GET" }).handler(
  async ({}, { context }) => {
    const results = await Promise.all(
      Object.entries(BUDGETS).map(async ([metric, budget]) => {
        const row = await context.env.DB.prepare(`
          SELECT AVG(value) as avg
          FROM web_vitals
          WHERE name = ? AND created_at > datetime('now', '-1 day')
        `).bind(metric).first()

        return {
          metric,
          current: row.avg,
          budget,
          passing: row.avg <= budget,
        }
      })
    )

    return results
  }
)

Zusammenfassung der SaaS-spezifischen Optimierung

KomponenteBetroffene MetrikSaaS-spezifische Lösung
Dashboard-DiagrammeLCP, INPSkeleton-Loading, verzögertes Rendern unterhalb des sichtbaren Bereichs
DatentabellenCLS, INPFeste Spaltenbreiten, virtuelles Scrollen
Authentifizierungs-RedirectLCPPrefetch des Auth-Status, Inhalte sofort anzeigen
PreistabellenCLSKarten mit fester Höhe, Platz für Jahres-Toggle reservieren
SuchergebnisseINPEntprellte (debounced) Eingabe, virtuelle Liste
BenachrichtigungenCLSAbsolut positioniertes Toast, keine DOM-Einfügungs-Verschiebungen
ModaleCLSFeste Positionierung, Scrollen des Hintergrunds verhindern

Fazit

Core Web Vitals sind nicht nur ein SEO-Rankingfaktor – sie haben direkte Auswirkungen auf die Benutzerzufriedenheit und die Konversionsraten. Für SaaS-Anwendungen erfordert die Optimierung dieser Metriken einen systematischen Ansatz:

  1. Messen Sie Web Vitals in der Produktion mit Real User Monitoring
  2. Legen Sie Budgets fest für jede Metrik und richten Sie Alarme ein, wenn diese überschritten werden
  3. Optimieren Sie LCP durch serverseitiges Rendern, Preloading und Bildoptimierung
  4. Eliminieren Sie CLS durch explizite Abmessungen, reservierten Platz und vorhersehbare Layouts
  5. Verbessern Sie INP durch schrittweise Berechnungen, zögerliches Laden (Lazy Loading) und Optimistic UI

Der Vorteil von TanStack Start auf Cloudflare Workers: Streaming SSR reduziert den LCP, indem Inhalte schneller bereitgestellt werden, und die Edge-native Architektur gewährleistet weltweit eine niedrige TTFB. Für einen tiefen Einblick in die Implementierung von Streaming SSR, lesen Sie unseren Leitfaden für Streaming SSR. Um Ihre Web Vitals kontinuierlich in der Produktion zu verfolgen, richten Sie Real User Monitoring mit unserer Schritt-für-Schritt-Anleitung ein.