TTFB vs FCP: What Really Matters for SEO

A common audit mistake is treating Time to First Byte and First Contentful Paint as ranking factors in their own right and “optimizing” whichever number a synthetic tool flags. Neither is a ranking input. As established in the parent FCP & TTFB Analysis work, both are diagnostic precursors: they sit on the critical path that produces Largest Contentful Paint, and LCP — alongside Interaction to Next Paint and Cumulative Layout Shift — is what actually feeds Google’s page-experience signal. This page shows how to read TTFB and FCP from field data, locate which one is dragging LCP, and decide what to fix first.

TTFB to FCP to LCP critical-path chain TTFB and FCP are diagnostic precursors on the critical path. Only LCP, INP and CLS feed Google's page-experience ranking signal. TTFB server + network Good ≤ 800 ms FCP first render Good ≤ 1.8 s LCP largest paint Good ≤ 2.5 s diagnostic precursors (not ranking factors) Page-experience signal LCP · INP · CLS ranking input Fix order 1. TTFB > 800 ms — infrastructure / CDN 2. FCP > 1.8 s with good TTFB — render path
TTFB and FCP gate LCP, but only LCP, INP and CLS feed the page-experience signal. See LCP Measurement & Optimization.

Prerequisites

Before you can decide which metric to fix, confirm these are in place:

  • A field-data source for both metrics — either your own Real-User Monitoring beacon collection or the CrUX API, aggregated at p75 over a 28-day window.
  • The web-vitals npm library wired into the page, or a direct PerformanceObserver setup against the navigation and paint entry types.
  • Server-Timing response headers emitted at the origin and preserved through every proxy hop, so you can split TTFB into origin compute versus edge transit.
  • Agreement that the headline number is p75, not an average — a mean hides the slow tail that ranking thresholds are evaluated against.

Thresholds and what each metric actually tells you

TTFB and FCP have their own “Good” boundaries, but those boundaries are diagnostic gates, not ranking lines. The ranking-relevant thresholds belong to LCP, INP and CLS.

Metric Good (p75) Needs improvement Poor Ranking factor? What it isolates
TTFB ≤ 800 ms ≤ 1800 ms > 1800 ms No (precursor) Origin compute + network/TLS transit
FCP ≤ 1.8 s ≤ 3.0 s > 3.0 s No (precursor) Critical-path parse + first render
LCP ≤ 2.5 s ≤ 4.0 s > 4.0 s Yes Largest element paint time
INP ≤ 200 ms ≤ 500 ms > 500 ms Yes Interaction responsiveness
CLS ≤ 0.1 ≤ 0.25 > 0.25 Yes Visual stability

The chain is strict and additive: TTFB → FCP → LCP. Every millisecond of TTFB is a millisecond LCP cannot beat, because no pixel can paint before the first byte arrives. FCP then adds the critical-path render cost on top. If TTFB is 1.2 s, an LCP of 2.5 s is already impossible regardless of how lean the render path is. That dependency is why fixing the wrong one wastes effort.

How to diagnose TTFB vs FCP and decide what to fix first

Step 1 — Capture both metrics from the field with attribution

Capture TTFB and FCP with the web-vitals library, which normalizes the underlying PerformanceNavigationTiming and paint entries across browsers. Tag each beacon with connection type so you can later separate server latency from client conditions.

import { onTTFB, onFCP } from 'web-vitals';

const SAMPLING_RATE = location.pathname.startsWith('/checkout') ? 1.0 : 0.15;

function sendBeacon(metric) {
  if (Math.random() > SAMPLING_RATE) return;

  const payload = {
    name: metric.name,                       // 'TTFB' | 'FCP'
    value: Math.round(metric.value),         // ms; TTFB = responseStart - requestStart
    rating: metric.rating,                   // 'good' | 'needs-improvement' | 'poor'
    navigationType: metric.navigationType,
    ect: navigator.connection?.effectiveType || 'unknown',
    path: location.pathname,
  };

  navigator.sendBeacon('/rum-collector', JSON.stringify(payload));
}

onTTFB(sendBeacon);
onFCP(sendBeacon);

Why: Both metrics fire once per navigation, so there is no reportAllChanges benefit. Sending connection type lets you avoid blaming the server for what is really a slow 3G client. Beacons flush reliably on the unload path, so you do not lose the tail.

Step 2 — Split TTFB into origin compute and transit with Server-Timing

A high TTFB is either slow origin work or slow network. Server-Timing headers let the browser attribute the split without guessing.

// At the edge / reverse proxy, emit:
//   Server-Timing: origin;dur=120, edge;dur=18, cache;desc=MISS
function readServerTiming() {
  const [nav] = performance.getEntriesByType('navigation');
  const transit = nav.responseStart - nav.requestStart;          // total TTFB
  const breakdown = nav.serverTiming.reduce((acc, e) => {
    acc[e.name] = e.duration;                                    // origin, edge, ...
    return acc;
  }, {});
  const compute = breakdown.origin || 0;
  return { transit, compute, network: Math.max(0, transit - compute) };
}
console.table(readServerTiming());

Why: If compute dominates, the bottleneck is database queries, render-blocking SSR, or cold edge workers — fix at the origin. If network dominates, the bottleneck is TLS/handshake or geographic distance — fix with edge caching and a closer point of presence.

Step 3 — Confirm FCP is render-blocking, not server-bound

Subtract TTFB from FCP. The remainder is pure critical-path render time. If TTFB is good but the remainder is large, the problem is render-blocking CSS, synchronous JavaScript, or font loading.

const [nav] = performance.getEntriesByType('navigation');
const fcp = performance.getEntriesByName('first-contentful-paint')[0];
const ttfb = nav.responseStart - nav.requestStart;
const renderCost = fcp ? Math.round(fcp.startTime - ttfb) : null;

const blocking = performance.getEntriesByType('resource')
  .filter(r => (r.initiatorType === 'link' || r.initiatorType === 'script') &&
               r.startTime < (fcp ? fcp.startTime : Infinity))
  .map(r => ({ url: r.name, ms: Math.round(r.duration), type: r.initiatorType }));

console.log({ ttfb: Math.round(ttfb), renderCost, blocking });

Why: renderCost separates “the server was slow” from “the browser was busy parsing blockers.” The blocking list names the exact stylesheets and scripts that landed before FCP, so the fix (inline critical CSS, defer, font-display: swap) targets real resources rather than guesses.

Step 4 — Decide the fix order

Apply the rule the data now supports:

  1. TTFB p75 > 800 ms → fix first. It taxes every downstream metric and throttles crawl throughput for large sites. Attack origin compute or transit per Step 2.
  2. FCP p75 > 1.8 s with good TTFB → fix second. This is a critical rendering path problem; unblock the parser per Step 3.
  3. Both within budget but LCP poor → the issue is the LCP element itself (large image, late discovery), handled in LCP Measurement & Optimization, not here.

Verifying it works

  • DevTools: Open the Network panel, hover the document request, and read the Timing tab — “Waiting for server response” is TTFB. In the Performance panel, the FCP marker should move left after a render-path fix.
  • Console: Re-run the Step 3 snippet. After inlining critical CSS, renderCost should drop and the blocking array should shrink toward empty.
  • RUM dashboard: Watch the p75 TTFB and p75 FCP series over the next 28-day window. A genuine fix shows a sustained downward step, not a one-day dip. Confirm p75 LCP moves in step — that is the metric that actually changes the page-experience signal.
  • CrUX cross-check: Compare your internal p75 against the CrUX API p75 for the same origin. Variance above 15% means your sampling or field source needs review before you trust the verdict.

Edge cases & gotchas

  • Cache hits collapse TTFB to near zero, making field TTFB bimodal. Segment by cache;desc=HIT|MISS from Server-Timing or your p75 will average two unrelated populations.
  • Redirects inflate TTFB if measured naively. responseStart - requestStart excludes redirect time by design; do not switch to responseStart - startTime, which folds redirects back in.
  • Bfcache restores report TTFB ≈ 0 and may skip FCP. Check navigationType === 'back-forward-cache' and exclude those entries from origin-latency analysis.
  • Safari historically lacked some paint-timing support, so FCP coverage can be thinner there; rely on the web-vitals library’s normalization rather than reading paint entries raw.
  • Prerender and early-hints (103) responses can make FCP appear before a “real” TTFB; trust the navigation entry’s responseStart, not wall-clock intuition.
  • A low FCP with a high LCP is not a TTFB or FCP problem at all — the first paint was a header or skeleton, and the real content element is late. Re-route that case to LCP work.

FAQ

Is TTFB or FCP a Google ranking factor?

Neither is. The page-experience signal is fed by LCP, INP and CLS. TTFB and FCP are diagnostic precursors that gate LCP, so improving them helps ranking only indirectly by improving LCP.

Which should I fix first, TTFB or FCP?

Fix TTFB first if its p75 exceeds 800 ms, because it delays every downstream metric. Then fix FCP if its p75 exceeds 1.8 s while TTFB is healthy — that remainder is render-blocking critical-path cost.

Why does my synthetic tool show a much lower TTFB than my RUM data?

Synthetic runs reuse warm DNS, persistent TCP connections and idealized routing. Field data captures real TLS handshakes, mobile networks and edge variance, so always decide from p75 field data, not lab numbers.