Fixing Slow First Contentful Paint (FCP)
First Contentful Paint is the moment a visitor sees anything at all from your page: a line of text, a logo, a background image. Until then they are looking at a white screen and wondering whether the link worked.
FCP is not a Core Web Vital and does not decide a pass or fail on its own, but a slow one almost always drags Largest Contentful Paint down with it. This guide explains what has to happen before the first paint, how to see which item is holding it up on your page, and how to clear each one.
What counts as the first contentful paint
The browser records FCP the first time it draws any content from the page's DOM. Text counts, as long as it is actually visible. So do images, including CSS background images, SVG elements and canvas elements that have been drawn on. A plain background colour does not count, and neither does text that the browser is holding back while a web font downloads.
Google's guidance rates FCP like this, using the 75th percentile of visits:
| FCP | Rating |
|---|---|
| 1.8 s or less | Good |
| Over 1.8 s up to 3 s | Needs improvement |
| Over 3 s | Poor |
FCP appears in the Chrome UX Report and in PageSpeed Insights, but it is a diagnostic measurement. Search Console's Core Web Vitals report is based on LCP, INP and CLS, not FCP. The reason to fix FCP is what it tells you: if nothing at all appears for three seconds, the largest element cannot possibly appear within 2.5, and visitors on slow connections may leave before they see anything.
What must happen before anything is painted
Between the click and the first paint, the browser works through a fixed sequence. Every item on this list adds to FCP, and any one of them can be the slow part:
- Redirects. Each hop, for example from an advert's tracking link to http, then https, then the www version, is a full round trip before your page is even requested.
- Connection and server response. DNS lookup, connection set-up, TLS, and then the time your server takes to produce the HTML. Together these make up Time to First Byte.
- Downloading and parsing the HTML head. The browser reads the head from top to bottom. A very large head, packed with inline scripts, tracking tags and inline SVG sprites, takes longer to arrive and to read.
- Render-blocking stylesheets. Every
<link rel="stylesheet">that applies to the current screen must be downloaded and parsed before anything is drawn. Stylesheets pulled in with@importare worse, because the browser only finds them after reading the first file. - Parser-blocking scripts. A
<script src>withoutdeferorasyncin the head stops the browser reading the HTML until the script has downloaded and run. - Something visible to paint. If the body is empty until JavaScript builds it, or is hidden by a style rule, or all its text is waiting for a font, the browser has nothing to show even when it is ready.
A fast site keeps this path short: one quick response, one small stylesheet or inlined critical rules, no blocking scripts, and real text in the HTML.
Finding your blocker
Run the page through the Core Web Vitals checker and note FCP from the field data and the lab run, alongside TTFB. Subtract TTFB from FCP. What remains is time the browser spent after the HTML started arriving, and that is time you can usually win back in the front end.
In Chrome DevTools
- Open the Network panel, tick Disable cache, and set throttling to a slow 4G profile.
- Reload and look at the first few requests. Redirects show as 301 or 302 rows before the document. Look for stylesheets and scripts requested from the head, and note which have Highest or High priority.
- Switch to the Performance panel and record a reload. The FCP marker appears in the Timings track. Everything on the main thread and the network before that marker is a candidate blocker.
- Open the Coverage tab from the command menu and reload. It shows how much of each CSS and JavaScript file was actually used for the first view. A 300 KB stylesheet with 90% unused code is a strong sign that critical CSS will help.
Lighthouse lists the specific offenders in its render-blocking diagnostics, with an estimate of the time each one costs. Treat the estimates as a guide to priority rather than an exact saving.
A quick test for hidden content
Load the page with JavaScript switched off in DevTools settings. If you get a blank screen, the page relies on scripts to show anything, and FCP can never be earlier than the time those scripts take to download and run. If you see content but it is invisible when JavaScript is on, look for a hiding snippet.
Critical CSS in practice
Critical CSS means the small set of rules needed to draw the first screenful: the header, navigation, hero area and the typography of the opening text. Put those rules inline in the head, then load the full stylesheet without blocking rendering.
<head>
<style>
/* critical rules for header, hero and first text block only */
body{margin:0;font-family:system-ui,sans-serif;color:#1d2433}
.site-header{display:flex;align-items:center;height:64px;padding:0 16px}
.hero h1{font-size:2rem;line-height:1.2;margin:24px 16px}
</style>
<link rel="preload" href="/css/site.css" as="style">
<link rel="stylesheet" href="/css/site.css" media="print" id="site-css">
<script>
const css = document.getElementById('site-css');
css.addEventListener('load', () => { css.media = 'all'; });
</script>
<noscript><link rel="stylesheet" href="/css/site.css"></noscript>
</head>
The media="print" trick makes the browser fetch the stylesheet at low priority without
blocking the screen render, and the tiny script switches it on for screens once it has loaded. The noscript
fallback keeps the page styled for visitors without JavaScript. Keep the inline block small, ideally
a few kilobytes, because inlined CSS cannot be cached between pages.
- Replace any
@importinside stylesheets with separate link tags, or combine the files at build time. - Give stylesheets that only apply to print or to wide screens a matching
mediaattribute so they do not block small screens. - Serve CSS from your own domain where you can. Each extra host needs its own connection before the file can download.
Scripts, hiding snippets and fonts
Scripts in the head
<!-- blocks parsing and rendering -->
<script src="/js/app.js"></script>
<!-- downloads in parallel, runs after the HTML is parsed, in order -->
<script src="/js/app.js" defer></script>
<!-- downloads in parallel, runs as soon as it arrives, order not kept -->
<script src="https://analytics.example.com/tag.js" async></script>
Use defer for your own scripts that depend on each other, and async for
independent third-party tags. Modules loaded with type="module" are deferred by default.
Anti-flicker and page-hiding snippets
Some A/B testing and personalisation tools ask you to add a snippet that hides the whole page, often by setting the body's opacity to zero, until the experiment script has loaded, with a timeout of several seconds as a safety net. On a slow connection the visitor stares at a blank screen for the full timeout. If you must keep such a tool, shorten the timeout, hide only the element being tested rather than the whole page, or run experiments on the server so no hiding is needed.
Web fonts
By default, most browsers hide text for a short period while a web font downloads. If all the visible
content is text in that font, nothing counts as painted. Set font-display: swap so fallback
text is drawn at once, preload the one or two font files used in the first screen, and self-host fonts
so they come from the same connection as the page.
@font-face{
font-family:"Harbour Sans";
src:url("/fonts/harbour-sans-600.woff2") format("woff2");
font-weight:600;
font-display:swap;
}
Platform notes
- WordPress. Optimisation plugins can generate critical CSS, delay scripts and add
font-displayfor you. Turn features on one at a time and check the result, because combining minification, delay and critical CSS from two plugins at once often breaks layouts. Page builders tend to load large stylesheets on every page; check the Coverage tab. - Shopify. Apps can add scripts and stylesheets to the theme head. The theme's own
CSS is usually well structured, so look first at app embeds and snippets added to
theme.liquid. - Single-page apps. A React, Vue or Angular app that renders only in the browser sends an empty body, so FCP waits for the whole bundle. Server-side rendering or static pre-rendering is the real fix; a lightweight HTML loading state is a stopgap.
- Static sites. You have full control. Inline critical CSS at build time, defer everything else, and serve from a CDN so TTFB stays low.
The fix, step by step
Step 1
Measure FCP and separate server time
Test the page in the Core Web Vitals checker and write down field and lab FCP next to Time to First Byte for mobile. Subtract TTFB from FCP to see how much time the browser spends after the HTML starts arriving. If TTFB alone is over about 800 ms, or the page goes through redirects before loading, fix the server and the redirect chain first, because no front-end change can paint content before the HTML arrives.
Step 2
List what blocks the first render
Reload
the page with the Network panel open, cache disabled and slow 4G throttling on. Write down every stylesheet
and every script without defer or async requested from the head, and any @import
chains. Use the Coverage tab to see how much of each file the first screen actually needs. This list,
not a generic checklist, is what you work through in the next three steps, starting with the largest
items.
Step 3
Inline critical CSS and load the rest later
Extract the rules needed for the header, navigation, hero and opening text of each
template and put them in a small inline style block in the head. Load the full stylesheet with the print-media
pattern or a preload so it no longer blocks rendering. Replace @import with link tags, add
media attributes to print-only files, and test for unstyled flashes on mobile and desktop.
Step 4
Move scripts out of the way
Add
defer to your own scripts in the head, or move them to the end of the body, and load independent
third-party tags with async. Remove scripts that no longer do anything, such as old chat
widgets or retired analytics. If a script genuinely must run before paint, inline it if it is tiny, and
check whether the work could be done on the server instead.
Step 5
Remove hide-the-page snippets
Search
the page source for styles that set the body or a wrapper to opacity:0 or visibility:hidden,
often added by A/B testing, personalisation or fade-in effects. Remove them if the tool is no longer
in use. Otherwise cut the timeout to the minimum, hide only the element under test, or move experiments
to the server so visitors are never shown a deliberately blank page while a script loads.
Step 6
Let text show while fonts load
Add
font-display: swap to every @font-face rule, or the equivalent parameter for
hosted font services, so fallback text is drawn immediately. Preload only the one or two font files used
above the fold, self-host them on your own domain, and choose a fallback font with similar proportions.
Check the result with slow throttling, where the difference between hidden and visible text is easiest
to see.
Step 7
Send real content in the HTML
View the source of the page and check that the header and opening text are present as HTML rather than built by JavaScript. For apps that render only in the browser, switch key templates to server-side rendering or static generation. Then rerun the lab test several times, confirm FCP has moved closer to TTFB, and watch field data over the following four weeks.
Keeping the first paint fast
- Set a head budget. Agree a limit on how many blocking files the head may contain, for example one stylesheet and no synchronous scripts, and check new templates against it.
- Review every new tag. Marketing, chat and testing tools are the most common way
blocking scripts creep back. Ask whether each one can load with
async, after consent, or after the first interaction. - Regenerate critical CSS. When a template's header or hero design changes, regenerate its critical rules as part of the build rather than leaving stale ones in place.
- Watch FCP alongside LCP. In your real-user monitoring, record FCP and TTFB as well as the Core Web Vitals, so you can see immediately whether a regression is in the server, the head or the hero.
For deeper coverage of the files themselves, see the render-blocking resources guide, and for the server side, the Time to First Byte guide.
1. Is First Contentful Paint a Core Web Vital?
No. The Core Web Vitals are Largest Contentful Paint, Interaction to Next Paint and Cumulative Layout Shift. FCP is a supporting measurement that Google reports in the Chrome UX Report and PageSpeed Insights, and Lighthouse includes it in its lab score, but it is not part of the Core Web Vitals assessment in Search Console. It still matters, because a slow first paint usually means a slow LCP, and a blank screen is when visitors are most likely to give up.
2. What is a good FCP time?
Google rates an FCP of 1.8 seconds or less as good, between 1.8 and 3 seconds as needing improvement, and over 3 seconds as poor, measured at the 75th percentile of real visits. Because FCP includes redirects and server response time, a page on slow hosting can miss the target however tidy its front-end code is. Compare FCP with Time to First Byte to see how much of the delay is under your front-end control.
3. Why is my page blank for several seconds before anything appears?
The browser is waiting for something before it will draw. The usual suspects are a slow server response or redirects, stylesheets and synchronous scripts in the head, a page-hiding snippet from an A/B testing tool, text held back while a web font loads, or a page that is built entirely by JavaScript. Load the page with JavaScript switched off and with slow throttling in DevTools to see which of these applies to your site.
4. Does async or defer make a difference to FCP?
Yes, often a large one. A plain script tag in the head stops the browser reading the rest of the HTML until that script has downloaded and run, so nothing can be painted in the meantime. Adding defer lets the file download in parallel and run after the HTML is parsed, keeping the order of scripts. Adding async also downloads in parallel but runs the script as soon as it arrives. Either removes the script from the path to the first paint.
5. Is inlining all my CSS a good idea?
Only for small sites with very little CSS. Inlining removes a network request, which helps the first paint, but inline CSS is sent again with every page and cannot be cached by the browser. For most sites the better pattern is to inline only the few kilobytes needed for the first screen and load the full stylesheet without blocking rendering. That keeps the first view fast and later pages light because the main file is cached.
6. Can a cookie banner slow down FCP?
It can, depending on how it is built. A consent tool loaded as a synchronous script in the head blocks the first paint until it arrives, and some banners hide the page until a choice is made. A well-built banner loads asynchronously and draws on top of content that is already visible. The banner itself can count as the first content painted, which is fine, as long as the script behind it does not delay everything else.
7. Why does Lighthouse show a different FCP from the field data?
Lighthouse loads the page once with an empty cache, on a simulated mid-range phone and a throttled connection. Real visitors include people with the site already cached, people on fast broadband, and people on poor mobile signal, and field data takes the 75th percentile over 28 days. The two often differ by a second or more. Use the lab figure to test changes and the field figure to judge what visitors experience.
The Core Web Vitals checker shows real-user field data from the Chrome UX Report and a Lighthouse lab test side by side for your URL, and points to the assets and code behind each slow result.
Open Core Web Vitals Fixes โ