First Contentful Paint (FCP) Optimization: The Complete 2026 Guide

Learn how to optimize First Contentful Paint below 1.8s at p75. Covers TTFB reduction, render-blocking CSS, web fonts, 103 Early Hints, and Speculation Rules prerender with production code.

Updated: August 20, 2026

First Contentful Paint (FCP) optimization means driving the time from navigation start to the browser painting the first text, image, SVG, or non-white canvas below 1.8 seconds at the 75th percentile of real users. FCP is a diagnostic metric (not one of the three Core Web Vitals), but it acts as the hard floor for Largest Contentful Paint (LCP), so a slow FCP guarantees a slow LCP. I profile FCP by attributing every millisecond between the connect handshake and the first paint frame on the compositor thread, and then killing whatever is blocking that frame. Honestly, once you've done a few of these audits, you start seeing the same three or four culprits every single time.

  • Good FCP is < 1.8s, needs-improvement is 1.8–3.0s, and poor is > 3.0s, all measured at the 75th percentile of field data.
  • FCP isn't a Core Web Vital, but it's a lower bound on LCP (LCP can never be faster than FCP).
  • Time to First Byte (TTFB) is usually the single largest contributor; anything over ~800 ms of TTFB makes a sub-1.8 s FCP effectively impossible on cold loads.
  • Render-blocking CSS, synchronous scripts in <head>, and web fonts without font-display: swap are the top three front-end delays.
  • Use the paint entry in PerformanceObserver, the Long Animation Frames API, and Chrome DevTools' Performance Insights panel to attribute FCP to a specific thread.
  • 103 Early Hints and Speculation Rules prerender both move FCP earlier on the timeline without changing the actual work the browser does.

What is First Contentful Paint?

First Contentful Paint marks the first paint frame after navigation in which the browser rendered any DOM content: text, an image (including a background image), a non-white <canvas>, or an SVG element. It excludes iframes and anything painted purely as background of the root element. The metric is defined by the W3C Paint Timing specification and is exposed to JavaScript through PerformanceObserver with entry type paint and name first-contentful-paint.

Mechanically, FCP happens when the compositor thread produces a frame that the display can present. Before that frame can exist, the main thread has to have parsed enough HTML to build the initial layout tree, blocked on any CSS in <head>, executed any synchronous scripts, and passed a display list to the compositor. If any of those steps stall, FCP slips. That's why I always trace FCP on two axes: the network waterfall up to the first paint, and the main-thread work between HTML parse-complete and the paint frame itself.

FCP is a strictly earlier event than LCP, and it's an earlier event than time-to-interactive too. It's not the same as First Paint. First Paint fires on any non-blank pixel including background colors, which is why Chrome deprecated it as a headline metric in 2020 and standardized on FCP instead.

What is a good FCP score in 2026?

A good First Contentful Paint score in 2026 is under 1.8 seconds at the 75th percentile of real users, segmented by device class. 1.8 s to 3.0 s is "needs improvement," and anything past 3.0 s is poor. These thresholds have been stable since Google published them in 2020 and remain unchanged in the current web.dev FCP guide. The 75th-percentile rule matters because a p50 that looks great often hides a tail of 4-second mobile loads that Google's Chrome UX Report will happily count against you.

In my experience auditing sites in 2026, three cohorts routinely miss the 1.8 s bar even when their p50 looks fine: users on 4G with a cold cache, users in regions more than 100 ms away from your origin without a real CDN, and users on low-end Android devices where the main-thread parse cost of a large stylesheet dominates. A good FCP number is one that holds up on a mid-tier Android phone with a throttled 4G profile, not one that only clears on your MacBook over fiber.

FCP contributes roughly 10 percent to the Lighthouse Performance score, but its indirect effect is much larger. It forms a hard lower bound for LCP and Speed Index, both of which have much higher weight, and one of which is a ranking signal.

FCP vs LCP: how they differ and why FCP still matters

FCP fires the first time any content pixel hits the screen. LCP fires when the largest above-the-fold element is painted. Because LCP includes the initial render pass, LCP is always ≥ FCP. Fixing FCP is therefore the cheapest way to move the LCP floor down, especially on sites where the LCP element is the same as the first painted element (a hero image or above-the-fold heading).

DimensionFirst Contentful PaintLargest Contentful Paint
Good threshold (p75)< 1.8 s< 2.5 s
Core Web Vital?No (diagnostic)Yes (ranking signal)
What it measuresAny DOM content paintedLargest above-the-fold element painted
Fires more than once?No, first paint onlyUpdates until first user input
Dominant cause of missesTTFB + render-blocking headLCP resource load + render delay
PerformanceObserver entrypaintlargest-contentful-paint

The rule I use on every audit: if FCP is bad, don't even look at LCP yet. Fix the paint floor first. For the deep LCP breakdown, see the Largest Contentful Paint sub-parts guide. There's a related pattern worth reading: the practical INP optimization guide covers how post-paint interactivity gets measured once the paint floor is fixed.

How to measure FCP in the lab and in the field

In the lab, I use Chrome DevTools' Performance panel with 4× CPU throttling and a "Slow 4G" network profile. The FCP marker appears as a green flag on the frames row. WebPageTest's filmstrip is the second lens I use, because it renders the actual pixel that fired FCP. That's helpful when your first paint is technically a menu bar you didn't intend to count (I hit exactly this on a client site last year and burned an afternoon before I noticed).

In the field, the web-vitals library exposes FCP via onFCP(). Here's a minimal RUM beacon that ships FCP with attribution to your analytics endpoint:

import { onFCP } from 'web-vitals/attribution';

onFCP((metric) => {
  // metric.value = FCP in ms
  // metric.attribution.timeToFirstByte = TTFB portion
  // metric.attribution.firstByteToFCP   = post-TTFB portion
  // metric.attribution.loadState        = 'loading' | 'dom-interactive' | 'dom-content-loaded' | 'complete'
  navigator.sendBeacon('/rum', JSON.stringify({
    name: 'FCP',
    value: metric.value,
    id: metric.id,
    ttfb: metric.attribution.timeToFirstByte,
    postTtfb: metric.attribution.firstByteToFCP,
    loadState: metric.attribution.loadState,
    url: location.pathname,
  }));
});

If you can't ship the library, you can subscribe directly with PerformanceObserver:

new PerformanceObserver((list) => {
  for (const entry of list.getEntriesByName('first-contentful-paint')) {
    console.log('FCP:', entry.startTime.toFixed(0), 'ms');
  }
}).observe({ type: 'paint', buffered: true });

For a longitudinal view across releases, wire the same beacon into a dashboard alongside the other Web Vitals. The web-vitals RUM setup guide walks the full pipeline. And for a debugging methodology that maps FCP directly to a specific thread stall, the Chrome DevTools Performance Insights panel auto-flags the responsible resource and event.

Reduce TTFB to unblock FCP

Time to First Byte is the single largest contributor to FCP for any origin that does server-side rendering, and it's the cheapest metric to attack because most of the wins are configuration, not code. FCP cannot start until the browser has the first byte of the HTML document, so anything you shave off TTFB is a direct, one-for-one gain on FCP.

My default TTFB-reduction checklist, ordered by impact:

  1. Move HTML rendering to the edge. Any origin more than 100 ms of RTT away from your user pays that RTT during TCP + TLS handshake before the request even leaves the browser. A worker-based edge (Cloudflare Workers, Deno Deploy, Vercel Edge) collapses that to single-digit ms.
  2. Cache the HTML. Even a 5-second edge cache with stale-while-revalidate is enough to drop TTFB by 80 percent on landing pages. See the stale-while-revalidate guide for the header patterns.
  3. Enable HTTP/3. The 0-RTT session resumption in QUIC saves a full round trip for repeat visitors and eliminates head-of-line blocking on lossy mobile networks. Details in the HTTP/3 and QUIC guide.
  4. Attribute TTFB to a specific server phase. Emit a Server-Timing header for each phase (DB, cache, render). I documented the exact pattern in the Server-Timing attribution guide.
  5. Preconnect to third-party origins early. Every third-party font, analytics, or ad origin costs a fresh handshake. Preconnecting during the HTML flush hides that cost inside the server think time.

Eliminate render-blocking CSS and JavaScript

Every CSS file in <head> is render-blocking by default. Every synchronous <script> in <head> is parser-blocking. Either one will push FCP out by the full duration of its network fetch, parse, and (for JS) execute. On a mid-tier Android over 4G, a 200 KB CSS file alone can cost 400–600 ms of FCP.

So, the three techniques that reliably move the needle:

1. Inline critical CSS

Ship the CSS needed to paint the above-the-fold viewport inline in the HTML, and load the rest asynchronously. This is the single highest-impact FCP fix on content sites. The extraction, tooling, and async-load pattern are covered end-to-end in the critical CSS extraction guide. The async-load pattern for the non-critical stylesheet is:

<link
  rel="stylesheet"
  href="/css/rest.css"
  media="print"
  onload="this.media='all'">
<noscript><link rel="stylesheet" href="/css/rest.css"></noscript>

2. Mark scripts as defer or async

Any script that doesn't need to run before FCP should be defer (in-order, after HTML parse) or async (whenever it arrives, order not guaranteed). Modern module scripts (type="module") default to defer. Anything without one of these attributes will block the parser.

<!-- Blocks parser and FCP -->
<script src="/js/app.js"></script>

<!-- Deferred: runs after HTML parse, in order -->
<script src="/js/app.js" defer></script>

<!-- Async: runs whenever it downloads -->
<script src="/js/analytics.js" async></script>

3. Split by media query

Stylesheets with a non-matching media attribute are downloaded but not render-blocking. Ship your print CSS with media="print", your dark-mode-only rules with media="(prefers-color-scheme: dark)", and your wide-viewport rules gated behind a matching query. The browser will still cache them for later but won't block FCP on their download.

Stop web fonts from delaying the first paint

Web fonts are the most-underestimated FCP killer I see in audits. If a font-family used by any text above the fold has no font-display declared, browsers will wait up to 3 seconds for the font to download before painting the text, turning an otherwise-fast FCP into a 3-second blank screen (the Flash Of Invisible Text, or FOIT).

The three-line fix is to always declare font-display: swap (or optional for a stricter policy) in every @font-face block:

@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2-variations');
  font-weight: 100 900;
  font-display: swap;   /* paint fallback immediately, swap when Inter arrives */
}

Beyond font-display, self-host your fonts, subset to the glyphs you actually use, and preload the one or two font files that render above-the-fold text. The full playbook, including how to size-adjust the fallback to eliminate the layout shift that swap can cause, is in the web font optimization guide.

Resource hints, Early Hints, and prerender

Three platform features let you either start network work earlier or move the paint frame itself earlier on the timeline. Used correctly they can pull FCP hundreds of milliseconds down. Used carelessly, they cause bandwidth contention that pushes FCP up.

preconnect and dns-prefetch

Preconnecting to a third-party origin (fonts, analytics, image CDN) pays the DNS + TCP + TLS cost during the server's think-time window instead of during the critical fetch. On a fresh connection this saves 100–300 ms depending on RTT. Limit to 3–4 origins, because the browser only holds a small number of hot connections. Full ranking of the four hint types in the resource hints comparison.

103 Early Hints

HTTP 103 lets your server flush Link: </css/app.css>; rel=preload headers during its own think time, before the 200 response is ready. The browser starts fetching those resources while the origin is still assembling HTML. This is the single most under-used FCP lever in 2026, because the origin cost is a few dozen lines of middleware. Setup is covered in the HTTP 103 Early Hints guide. If you're curious about the spec itself, the IETF RFC 8297 is short and readable.

Speculation Rules prerender

A prerendered document has already fired FCP before the user clicks. Activation just swaps in the pre-painted document, so field FCP appears near-zero. Use conservative eagerness modes to avoid wasting bandwidth on pages the user never visits. The full API surface is documented in the Chrome Speculation Rules reference.

Common FCP regressions I keep seeing in 2026

These are the FCP anti-patterns that show up in almost every audit I do:

  • Blocking A/B test snippets in <head>. A synchronous 40 KB experimentation loader in the head adds 200–500 ms to FCP on mobile. Move it below the critical CSS, or replace with a server-side variant.
  • Tag managers loaded synchronously. Even with GTM's own async loader, a container full of custom HTML tags fires synchronous scripts. Audit the container, not just the loader.
  • Client-side rendered above-the-fold text. If your hero heading is rendered by a React component, FCP waits for JS parse + hydration. Server-render the first viewport; hydrate below.
  • Large SVG sprites inlined in the HTML. A 200 KB inline SVG sprite pushes the HTML flush past 3 seconds. Reference sprite fragments via <use href="/sprite.svg#icon"> instead.
  • Cookie banners as blocking iframes. The banner's iframe steals a connection and often blocks the main-thread parser. Render the banner inline with a native <dialog>, or move it to the compositor with position: fixed after FCP.
  • Chained CSS @import statements. Each @import serializes a network round-trip. Concatenate at build time, or use <link rel="stylesheet"> so the browser can fetch in parallel.

When you fix these, retest under the same throttling profile you established your baseline on. Otherwise you'll chase a moving target and never know whether the improvement was real, or whether it was just a faster CPU on the CI runner.

Frequently Asked Questions

Is First Contentful Paint a Core Web Vital?

No. First Contentful Paint is a diagnostic metric, not one of the three Core Web Vitals (LCP, INP, CLS). It's included in Lighthouse and web-vitals because it's a lower bound for LCP and correlates strongly with perceived loading speed, but it doesn't directly affect Google search ranking. Fixing FCP still tends to improve LCP, which does.

What is the difference between FCP and First Paint?

First Paint fires on the first non-blank pixel of any kind, including a background color set on <body>. First Contentful Paint fires only when actual DOM content (text, images, SVG, or a painted canvas) is rendered. Chrome deprecated First Paint as a headline metric because a background color rarely represents useful content to the user.

Why is my First Contentful Paint slow even though my server is fast?

Fast TTFB but slow FCP almost always points to render-blocking resources in <head>: a large stylesheet, a synchronous script, a web font without font-display: swap, or a client-side rendered hero. Trace the gap between responseEnd and the FCP marker in DevTools; whatever runs on the main thread in that window is your culprit.

Does lazy loading images help First Contentful Paint?

Only if the lazy-loaded image was previously delaying the initial fetch of higher-priority resources. Native loading="lazy" doesn't defer above-the-fold images by default in Chrome. Never lazy-load your LCP image; it will hurt both LCP and FCP if that image was the first paint.

How much can 103 Early Hints improve FCP?

Field data from Shopify and Cloudflare shows 100–400 ms of FCP improvement on origins whose server-think time is more than 200 ms, because critical CSS and fonts start downloading during that think window. The gain scales with how much of your critical path is discoverable from the HTML head; sites with heavy inline CSS see smaller wins.

Alex Petrov
About the Author Alex Petrov

Web performance engineer who treats every millisecond as a personal challenge. Has profiled more sites than he can count.