News

TTFB im Shop optimieren: Was wirklich hilft (ohne Voodoo)

Von Erol Demirkoparan
7 min
TTFB im Shop optimieren: Was wirklich hilft (ohne Voodoo) - Cloudox Software Agentur Blog

Letzte Woche bei einem Kunden: Der Shop war „gefühlt langsam“, aber Lighthouse sah gar nicht so dramatisch aus. Dann haben wir ins Monitoring geschaut – und die TTFB war bei vielen Requests jenseits von Gut und Böse. 900ms, manchmal 1.6s. Und das Gemeine: Der Rest der Page war eigentlich okay. Der Nutzer wartet halt am Anfang, bevor überhaupt was passiert. So ein bisschen wie an der Supermarktkasse, wo keiner scannt, weil erst noch die Kasse bootet.

Problem Statement

In den meisten Projekten ist die Datenbankabfrage der Hauptflaschenhals

Beispiel: B2B Shopware 6 Shop (≈ 50k SKUs)

API Response Time von 600ms auf 180ms optimiert

Implementation Checklist

  • ✓ Requirements definiert
  • ✓ Architektur geplant
  • ✓ Tests geschrieben
  • ✓ Dokumentation erstellt
```mermaid graph LR A[Input] --> B[Process] B --> C[Output] ``` ```typescript // Configuration Example export const config = { environment: 'production', apiEndpoint: 'https://api.example.com', timeout: 5000, retries: 3 }; ```

Wenn die Time To First Byte im Shop hoch ist, fühlt sich alles zäh an – selbst wenn Bilder optimiert sind und die Frontend-Bundles schlank wirken. TTFB ist oft ein Mix aus „Server braucht zu lange“ und „wir machen zu viel, bevor wir überhaupt antworten“. Und ja: Im E‑Commerce knallt das besonders rein, weil Produktlisten, Preise, Verfügbarkeiten, Personalisierung, Gutscheine, Tracking-Server-Side, Consent… irgendwie hängt alles an allem.

Ein Fehler, den ich oft sehe: Wir optimieren wie wild am Frontend, aber der erste Byte kommt erst, wenn der Server drei Datenbanken, zwei APIs und ein Template-System durchgekaut hat. Da kann das schönste CSS nix retten.

Technical Background

TTFB setzt sich grob zusammen aus:

  • Netzwerk: DNS, TLS, Routing, ggf. schlechter Peering-Kram. Nicht sexy, aber real.
  • Queueing: Der Request hängt, weil der Server gerade busy ist (zu wenig Worker, zu viele Locks, zu wenig DB-Connections).
  • Server Processing: App-Code, SSR, Template-Rendering, API-Calls, DB-Queries.
  • Origin → DB: langsame Queries, N+1, fehlende Indizes, Transaktionen/Locks.

Ich bin nicht 100% sicher, ob jeder Shop das gleich merkt, aber bei mir war’s bisher fast immer so: TTFB ist ein Symptom. Du musst die heißeste Stelle finden, nicht überall ein bisschen drehen.

Implementation Steps

1) Erst messen, dann schrauben. Klingt banal. Ist aber der Unterschied zwischen „wir haben irgendwas getan“ und „wir haben TTFB wirklich runtergebracht“.

Wenn du Node/TypeScript im Backend hast (oder SSR), dann fang ich meist so an: wir markieren Server-Timings und korrelieren die mit Request-IDs. Damit siehst du im Browser (DevTools) direkt, ob’s DB, Rendering oder externe Calls sind.

// Express/Middleware Beispiel
import { randomUUID } from "crypto";
import type { Request, Response, NextFunction } from "express";

export function perfMiddleware(req: Request, res: Response, next: NextFunction) {
  const rid = randomUUID();
  const start = process.hrtime.bigint();

  res.setHeader("x-request-id", rid);

  // Server-Timing ist Gold wert, wenn man TTFB auseinandernehmen will
  res.on("finish", () => {
    const durMs = Number(process.hrtime.bigint() - start) / 1e6;
    // TODO: später optimieren: in echtes Metrics-System pushen
    console.log(JSON.stringify({ rid, path: req.path, status: res.statusCode, durMs }));
  });

  // Beispiel: wir hängen später noch Teilzeiten dran
  res.locals.timings = { rid, marks: [] as { name: string; ms: number }[] };
  next();
}

export async function mark<T>(res: Response, name: string, fn: () => Promise<T>): Promise<T> {
  const t0 = process.hrtime.bigint();
  const out = await fn();
  const ms = Number(process.hrtime.bigint() - t0) / 1e6;
  res.locals.timings.marks.push({ name, ms });
  res.append("Server-Timing", `${name};dur=${ms.toFixed(1)}`);
  return out;
}

2) Externe Calls raus aus dem kritischen Pfad. Personalisierung, Recommendations, Payment-Badges – alles super, aber nicht vor dem ersten Byte. Wenn’s nicht unbedingt sein muss: nachladen oder cachen. „Erst antworten, dann verfeinern“ ist hier’s Mantra.

3) Caching – aber richtig. Nicht nur CDN für Assets, sondern HTML/JSON dort, wo’s geht. Und wenn du personalisierte Seiten hast: dann halt mit Variants (z.B. nach Land/Währung), oder mit Edge Side Includes, oder du gehst auf „statischer Kern + personalisierte Inseln“.

// Beispiel: Cache-Control für eine Kategorieseite (nicht personalisiert)
// Stale-while-revalidate macht oft den größten Unterschied bei TTFB
app.get("/k/:slug", async (req, res) => {
  res.setHeader("Cache-Control", "public, s-maxage=300, stale-while-revalidate=600");

  const data = await mark(res, "db", () => categoryRepo.load(req.params.slug));
  const html = await mark(res, "render", () => renderCategoryPage(data));

  res.status(200).send(html);
});

4) DB-Queries entknoten. Bei Shops killt dich gerne die Produktliste: Sortierung, Filter, Preisregeln, Stock, plus irgendwelche Marketing-Labels. Wenn du TTFB senken willst, musst du die eine Query finden, die 300ms frisst, plus die fünf kleinen, die zusammen auch 300ms sind.

-- Typischer Kandidat: Produktliste ohne passenden Index
EXPLAIN (ANALYZE, BUFFERS)
SELECT p.id, p.slug, p.title, pr.price_cents
FROM products p
JOIN prices pr ON pr.product_id = p.id AND pr.currency = 'EUR'
WHERE p.category_id = $1
  AND p.is_active = true
ORDER BY p.popularity DESC
LIMIT 24 OFFSET 0;

-- Oft hilft: Index auf (category_id, is_active) + popularity, je nach DB
-- CREATE INDEX CONCURRENTLY idx_products_cat_active_pop
--   ON products(category_id, is_active, popularity DESC);

5) Warm-up & Connection Pools. Ich hab schon Shops gesehen, da war die TTFB nach Deploy 10 Minuten schlecht, weil Caches kalt und DB-Pools falsch konfiguriert waren. Und dann wundert man sich, warum Monitoring nachts grün und nachmittags rot ist.

# Kleines Script zum Warm-up wichtiger URLs nach Deploy
# (ja, ist stumpf, aber funktioniert halt)
import time
import requests

URLS = [
  "https://shop.example.com/",
  "https://shop.example.com/k/sneaker",
  "https://shop.example.com/k/jacken",
]

for url in URLS:
  t0 = time.time()
  r = requests.get(url, headers={"Cache-Control": "no-cache"}, timeout=10)
  dt = (time.time() - t0) * 1000
  print(url, r.status_code, f"{dt:.0f}ms")
  time.sleep(0.5)  # nicht direkt den Origin grillen

Best Practices (so wie wir’s im Alltag machen)

Server-Timing & Tracing an. Ohne das tappst du im Dunkeln. Wenn du schon APM hast (Datadog, New Relic, Elastic, OpenTelemetry): super. Wenn nicht, bau dir wenigstens Request-IDs und Timings ein.

SSR optimieren, nicht dogmatisch werden. Manchmal ist SSR für SEO wichtig, klar. Aber du musst nicht alles SSRn. Häufig reicht: above-the-fold SSR, Rest als Streaming/partials. Je nach Stack kannst du HTML streaming nutzen, damit der erste Byte früher rausgeht.

CDN auch für HTML nutzen. Viele trauen sich nicht, weil „Shop ist personalisiert“. Ja, stimmt oft – aber meistens nicht überall. Kategorien, CMS-Seiten, Ratgeber, sogar PDPs (wenn Preis/Stock via AJAX nachkommt) lassen sich oft super cachen.

Timeouts & Fallbacks bei externen Services. Nichts ist schlimmer als ein Recommendation-Service, der sporadisch 2 Sekunden hängt und dann deine TTFB hochzieht. Lieber hart timeouten und ohne ausliefern.

DB: weniger Magie, mehr Indizes. Und: N+1 vermeiden. Ich weiß, klingt wie aus’m Lehrbuch, aber in Shops ist das immer wieder der Übeltäter, weil irgendein Resolver pro Produkt noch „nur mal schnell“ Labels lädt.

Real-World Example

Bei einem Projekt letzten Monat hatten wir eine Kategorie-Seite, TTFB ~1.2s. Im Trace sah man: 400ms DB, 300ms ein Pricing-Service, 200ms Rendering, der Rest Queueing/Overhead. Wir haben dann:

  • Pricing für die Kategorie-Liste für 60 Sekunden gecacht (pro currency + customer-group).
  • Die DB-Query mit einem passenden Index gefixt und ein JOIN vereinfacht.
  • Den Recommendations-Call aus dem kritischen Pfad rausgenommen (nachladen).

Danach waren wir bei ~250–350ms TTFB am Origin und am CDN teilweise unter 100ms. War jetzt kein Hexenwerk, aber man muss halt konsequent sein. Übrigens: Das größte Drama war am Ende nicht der Code, sondern eine zu niedrige Worker-Anzahl im Container-Setup. Hat keiner auf dem Zettel gehabt, weil „CPU ist doch bei 40%“. Queueing ist halt fies.

Häufig gestellte Fragen

Autor

Erol Demirkoparan

Erol Demirkoparan

Senior Software Architect

Full-Stack & Cloud-Native Systems Expert. Spezialisiert auf AWS, Next.js und skalierbare SaaS-Architekturen. Building the future of automated SEO.

AWSNext.jsScalable SaaSSystem Architecture

Veröffentlicht am

9. Februar 2026

Das könnte Sie auch interessieren