First Contentful Paint (FCP) — what blocks it
FCP = when the first text/image/SVG paints. Blocked by: slow TTFB, render-blocking CSS, render-blocking synchronous JS in <head>, late-arriving HTML, large HTML. Fixes: CDN + SSR/SSG for fast HTML; inline critical CSS; defer JS; preconnect to third parties; minimize HTML; preload key fonts with font-display: swap.
FCP measures the time from navigation to when the first content (text, image, SVG, non-blank canvas) is painted. Target: < 1.8s at p75. It precedes LCP — fixing FCP usually also helps LCP.
What blocks FCP
1. Slow TTFB
If the server takes 1.5s to send the first byte, FCP can't be under 1.5s. Fix at the server: CDN edge caching, faster origin, HTTP/2/3.
2. Render-blocking CSS
The browser won't paint until all <link rel="stylesheet"> resources are downloaded and parsed.
3. Render-blocking JS in head
Synchronous <script> in <head> blocks HTML parsing AND CSS parsing AND painting.
4. Late HTML
If your HTML doesn't include any visible content (CSR shell only), there's nothing to paint until JS loads, parses, executes, and renders. FCP slips by seconds.
5. Heavy HTML
Huge HTML (hundreds of KB) takes time to download and parse.
6. Web fonts with font-display: block
The default block setting hides text until the font loads — the text exists in the DOM but isn't painted.
Fixes (in order of impact)
Fast HTML delivery
- CDN + edge caching — TTFB < 200ms on cached pages.
- SSR/SSG so the HTML includes visible content, not just
<div id="root"/>. - HTTP/2 or 3 for multiplexing.
- Compression (Brotli/gzip) on text.
Reduce render-blocking CSS
- Inline critical CSS for above-the-fold; load the rest async (
<link rel="preload" as="style">+onload="this.rel='stylesheet'"). - Split CSS so each page only loads what it needs.
mediaattribute on<link>to mark non-critical:<link rel="stylesheet" href="print.css" media="print">.
Defer JS
deferfor scripts that need DOM but not before parse.asyncfor independent scripts (analytics, polyfills).type="module"is deferred by default.- Code split — don't ship every route's JS in the entry bundle.
Fonts
font-display: swaporoptionalso text renders in a fallback font immediately.- Preload the critical font:
<link rel="preload" as="font" type="font/woff2" crossorigin href="...">. - Subset fonts to the glyphs you actually use.
Preconnect / DNS-prefetch
<link rel="preconnect" href="https://api.example.com" crossorigin>For critical third-party origins, opens the connection (DNS + TLS) ahead of the actual request.
FCP vs LCP
- FCP = first paint of anything (often the header / skeleton).
- LCP = first paint of the largest visible element (often the hero).
- Fixing FCP gives the user "the page is loading"; fixing LCP gives them "the page is here."
- A skeleton screen improves FCP but doesn't help LCP; both matter.
How to measure
- PageSpeed Insights / Lighthouse (synthetic).
- web-vitals library in production (RUM, p75).
PerformanceObserver:
new PerformanceObserver((list) => {
for (const e of list.getEntries()) {
if (e.name === "first-contentful-paint") console.log("FCP:", e.startTime);
}
}).observe({ type: "paint", buffered: true });Interview framing
"FCP is when the first visible content paints — target < 1.8s at p75. It's blocked by slow TTFB, render-blocking CSS (the browser won't paint until all stylesheets parse), render-blocking JS in head, late-arriving HTML, and font-display: block hiding text. The order-of-impact fixes are: get HTML to the browser fast (CDN, SSR/SSG, compression) — biggest lever; inline critical CSS and defer the rest; defer non-critical JS; use font-display: swap and preload critical fonts; preconnect to third-party origins. FCP and LCP are different metrics — FCP says 'the page is loading', LCP says 'the page is here' — but the techniques to fix them overlap heavily."
Follow-up questions
- •Why does render-blocking CSS block FCP specifically?
- •What's the difference between FCP and LCP?
- •How does font-display change FCP?
- •How does SSR change the FCP profile vs CSR?
Common mistakes
- •CSR-only app with no SSR — FCP waits for the JS bundle.
- •Synchronous third-party script in head (analytics) — blocks FCP.
- •Big CSS bundles with everything for every page.
- •Default `font-display: block` hiding text.
Performance considerations
- •FCP is mostly network + CSS work. Fixes are infrastructure + critical-CSS + font strategy. The wins are large and durable.
Edge cases
- •Mobile 3G with high latency — TTFB dominates.
- •Heavy HTML on a CMS-driven page.
- •Authenticated pages can't cache at the CDN as easily.
Real-world examples
- •Next.js automatic critical CSS inlining; Vercel edge caching.
- •Cloudflare-fronted sites with sub-200ms TTFB.