First contentful paint: what it measures and how to move it
First contentful paint is the moment the first text or image lands on screen. This chapter explains what the metric counts, the 1.8-second Lighthouse target, and the five head changes that pull the first paint earlier.

First contentful paint is the moment the browser paints the first piece of content on the screen — the first text, image or canvas, whichever lands first. Before it the screen is blank; after it a visitor can tell something is happening. Lighthouse scores it against a 1.8-second target, and it is usually the earliest of the loading metrics that a real person notices.
Read this first
You need access to your site's <head> and, ideally, to its server or CDN. No build step is required. This chapter assumes you already know why a stylesheet in the head delays the first paint; render blocking resources covers that mechanism. The number this work feeds into is largest contentful paint, and for what real sites actually ship, first contentful paint on 34 homepages is the measured companion to this chapter.
What first contentful paint actually counts
FCP is not the time until the page is useful. It is the time until anything has been drawn, which is why it is cheap to improve and easy to misread. The browser has to fetch the document, build a render tree, and then paint at least one text or image node; whatever blocks the render tree also pushes FCP back. On most sites that blocker is a stylesheet, because the browser will not paint text whose layout it cannot compute.
The metric is anchored to the navigation start, not to the first byte or to the first script. That distinction matters: a fast server with one blocking stylesheet is often slower on FCP than a slower server with the critical CSS inlined, because the server time is hidden inside the blocking wait. The table is what each stage contributes, and which of them FCP is waiting on.
| Stage | Ends when | Blocks FCP? |
|---|---|---|
| DNS, TCP, TLS | the connection is ready | Yes — nothing starts before it |
| Server response | the HTML starts arriving | Yes |
| Blocking stylesheets | the CSS is fetched and parsed | Yes — no paint without it |
| Synchronous scripts in head | the script runs to completion | Yes — parsing pauses |
| Deferred scripts and images | later | No |
First contentful paint is the cost of everything you put before the first pixel.
Two consequences follow. First, the two levers with the most room are your stylesheets and any synchronous script in the head; the rest is either unavoidable or already deferred. Second, because FCP fires as soon as any content appears, a page that paints a placeholder immediately can have a good FCP and still be slow to show what the reader wanted — which is what largest contentful paint measures instead.
Do it in this order
Five steps, each with a check you can run before moving on. The first one is free and tells you whether the others are worth doing.
- Measure before you touch anything. Run the page through PageSpeed Insights or Lighthouse and record the FCP value and the list of render-blocking URLs it names. Done when you have a number and a list, not a feeling.
- Cut the number of blocking stylesheets. Every
<link rel="stylesheet">in the head is a separate chain. Concatenate the ones that always load together, and move anything not needed above the fold — print styles, a carousel's CSS, a widget's CSS — out of the critical path. Done when the blocking count in the report has gone down. - Inline the critical CSS for the first screen. Extract the rules that style the header and hero, put them in a single
<style>block before the first stylesheet link, and let the full stylesheet load afterwards. Done when the first screen renders from the inline block with the network throttled. - Delay what does not block. Add
deferto classic scripts that must run but do not need to run before the first paint, and load non-critical stylesheets asynchronously with amediaswitch or a preload-then-swap pattern. Done when no synchronous script remains in the head. - Re-measure and keep the delta. Compare against the number from step one, on the same connection profile. Done when you can state what changed and by how much, and you have stored both readings.
The deliverable: a head you can audit in five lines
The checklist below is the one we apply to our own pages. It maps each line to the stage of the table above, so a failure points at one thing to fix rather than a general "make it faster".
| Check | Pass | Fix if |
|---|---|---|
| Stylesheets in head | one or two | more, and they always load together |
| Critical CSS inlined | yes | the first screen waits on a link |
| Sync scripts in head | none | a <script src> without defer |
| Hero image in markup | yes | it is injected by script |
| Fonts on the same origin | yes | a third-party font stylesheet in head |
If you want the raw count rather than a score, the block below returns the blocking stylesheet and synchronous script totals for any URL. It is a plain fetch and does not need a headless browser, so it matches what a crawler sees as well as what a browser sees.
curl -sL "$URL" | python3 -c '
import sys, re
h = re.search(r"<head[^>]*>(.*?)</head>", sys.stdin.read(), re.S|re.I).group(1)
css = [s for s in re.findall(r"<link\b[^>]*>", h, re.I)
if re.search(r"rel\s*=\s*[\"\x27]?stylesheet", s, re.I)
and not re.search(r"media\s*=\s*[\"\x27]?print", s, re.I)]
js = [s for s in re.findall(r"<script\b[^>]*>", h, re.I)
if re.search(r"\bsrc\s*=", s, re.I)
and not re.search(r"\b(defer|async)\b", s, re.I)
and not re.search(r"type\s*=\s*[\"\x27]?module", s, re.I)]
print("blocking stylesheets:", len(css), "sync scripts:", len(js))'
What goes wrong
Three failures are common, and none of them throws an error, which is why they survive for years.
- Inlining too much CSS. The critical block is downloaded inside the HTML and cannot be cached separately. Inline the first screen only; an inline block that grows past roughly 14 KB starts costing more than the request it removed, especially on a cold repeat visit.
- Deferring a script that paints the main content. If the hero or the product grid is rendered by script, deferring it moves the paint later, not earlier. Move that rendering into the HTML, or accept that this page's FCP is really a script metric.
- Swapping stylesheets without a fallback. A stylesheet loaded asynchronously has a moment where the page is unstyled. If it flashes before the swap, you have traded a slow paint for a visible reflow, which the reader notices more.
Common questions
What is a good first contentful paint time?
Lighthouse marks 1.8 seconds or faster as good and 3 seconds or slower as poor, measured on a throttled mobile profile. Treat those as the line to clear, not the goal: a fast FCP with a slow largest contentful paint still feels slow, because the reader is looking at something incomplete.
Is first contentful paint a ranking factor?
Not by itself. It is not one of the three Core Web Vitals, and no Google document assigns it a ranking weight. It matters because it is the first thing a visitor perceives and because the work that improves it — fewer blocking resources, inlined critical CSS — usually improves the metrics that are official.
Why is my first contentful paint slow even though the server is fast?
Because FCP waits on the render tree, not on the first byte. A fast response followed by one large render-blocking stylesheet will show a slow FCP. Check the blocking list before you upgrade the server: moving a single stylesheet out of the critical path can beat a faster origin.
Does inlining critical CSS break caching?
It can. The inline block is part of the HTML, so every page that uses it re-sends those rules and no browser caches them across pages. Keep the block small, scope it to the first screen, and keep the full stylesheet as a normal cached file that loads right after.
The boundary worth saying plainly: this chapter is about making the first thing appear sooner, not about making the page complete. We have not measured how any single change moves FCP on your site, because it depends on your markup, your CDN and your connection; on a warm cache the difference can be under a hundred milliseconds and hard to see. Where FCP stops being the useful number is the moment the reader is waiting for the largest element, not the first one. Doing this work alongside how QueryWin tracks page performance is reasonable, but re-measure step one first.
Part of the QueryWin handbook · Level 2


