First contentful paint (FCP)
Chrome 60+ Edge 79+ Firefox 84+ Safari 15+ Details
Quick check for First Contentful Paint (opens in a new tab), the metric that marks when the browser renders the first piece of DOM content, text, image, canvas, or SVG. FCP measures the user’s perception of whether the page is loading.
Unlike LCP, FCP fires as soon as any content appears, making it the earliest signal of loading progress. A slow FCP almost always points to TTFB or render-blocking resources.
FCP rating thresholds
| Rating | Time | Meaning |
|---|---|---|
| Good | ≤ 1.8s | Content appears quickly |
| Needs Improvement | ≤ 3s | Noticeable delay |
| Poor | > 3s | Users may perceive the page as broken |
Render-blocking resources? Use Find Render-Blocking Resources for a detailed analysis with recommendations.
Snippet
Run it from an AI agent: the prompt uses the CLI to measure any URL and report the result.
// FCP Analysis
// First Contentful Paint with render-blocking phase breakdown
// https://webperf-snippets.nucliweb.net
(() => {
const valueToRating = (ms) =>
ms <= 1800 ? "good" : ms <= 3000 ? "needs-improvement" : "poor";
const RATING = {
good: { icon: "🟢", color: "#0CCE6A" },
"needs-improvement": { icon: "🟡", color: "#FFA400" },
poor: { icon: "🔴", color: "#FF4E42" },
};
const ms = (val) => `${Math.round(val)}ms`;
new PerformanceObserver((list) => {
const fcpEntry = list.getEntriesByName("first-contentful-paint")[0];
if (!fcpEntry) return;
const navEntry = performance.getEntriesByType("navigation")[0];
// Prerendered pages: measure from activation, not from the start of the prerender
const activationStart = navEntry?.activationStart || 0;
const fcpTime = Math.max(0, fcpEntry.startTime - activationStart);
const rating = valueToRating(fcpTime);
const { icon, color } = RATING[rating];
const ttfb = Math.max(0, (navEntry?.responseStart ?? 0) - activationStart);
// Render-blocking resources (Chrome 107+, requires renderBlockingStatus API)
const resources = performance.getEntriesByType("resource");
const blockingResources = resources.filter(
(r) => r.renderBlockingStatus === "blocking"
);
const lastBlockingEnd = blockingResources.length
? Math.max(...blockingResources.map((r) => r.responseEnd)) - activationStart
: ttfb;
console.group(
`%cFCP: ${icon} ${(fcpTime / 1000).toFixed(2)}s (${rating})`,
`color: ${color}; font-weight: bold; font-size: 14px;`
);
// Phase breakdown
console.log("");
console.log("%cPhase Breakdown:", "font-weight: bold;");
console.log(` TTFB: ${ms(ttfb)}`);
if (blockingResources.length > 0) {
console.log(
` Render-blocking load: ${ms(Math.max(0, lastBlockingEnd - ttfb))}`
);
}
console.log(
` Render delay: ${ms(Math.max(0, fcpTime - lastBlockingEnd))}`
);
// Render-blocking summary
console.log("");
if (blockingResources.length > 0) {
const totalBlockingTime = Math.max(0, lastBlockingEnd - ttfb);
console.log(
`%c⚠ ${blockingResources.length} render-blocking resource(s) — adding ${ms(totalBlockingTime)} before paint`,
"color: #FFA400; font-weight: bold;"
);
blockingResources
.sort((a, b) => b.responseEnd - a.responseEnd)
.forEach((r) => {
const filename = r.name.split("/").pop().split("?")[0] || r.name;
const type = r.initiatorType === "link" ? "CSS" : "JS";
console.log(` [${type}] ${ms(r.duration)} ${filename}`);
});
console.log("");
console.log(
"%c→ Find Render-Blocking Resources: %chttps://webperf-snippets.nucliweb.net/Loading/Find-render-blocking-resources",
"color: #3b82f6;",
"color: #3b82f6; text-decoration: underline;"
);
} else {
console.log(
"%c✓ No render-blocking resources detected",
"color: #22c55e;"
);
}
console.groupEnd();
}).observe({ type: "paint", buffered: true });
// Synchronous return for agent
const fcpEntrySync = performance.getEntriesByName("first-contentful-paint")[0];
if (!fcpEntrySync) return { script: "FCP", status: "error", error: "No FCP entry yet" };
const navEntrySync = performance.getEntriesByType("navigation")[0];
const activationStartSync = navEntrySync?.activationStart || 0;
const fcpTimeSync = Math.max(0, fcpEntrySync.startTime - activationStartSync);
const ratingSync = valueToRating(fcpTimeSync);
// Phases, measured from activation like the value: TTFB, then the wait for the last
// render-blocking resource, then the rest until the paint
const ttfbSync = Math.max(0, (navEntrySync?.responseStart ?? 0) - activationStartSync);
const blockingSync = performance
.getEntriesByType("resource")
.filter((r) => r.renderBlockingStatus === "blocking");
const lastBlockingEndSync = Math.max(
ttfbSync,
...blockingSync.map((r) => r.responseEnd - activationStartSync)
);
const ttfbMs = Math.round(ttfbSync);
const renderBlockingLoadMs = Math.round(lastBlockingEndSync - ttfbSync);
const renderDelayMs = Math.max(0, Math.round(fcpTimeSync) - ttfbMs - renderBlockingLoadMs);
return {
script: "FCP",
status: "ok",
metric: "FCP",
value: Math.round(fcpTimeSync),
unit: "ms",
rating: ratingSync,
thresholds: { good: 1800, needsImprovement: 3000 },
details: {
ttfbMs,
renderBlockingLoadMs,
renderDelayMs,
blockingResourceCount: blockingSync.length,
},
// Render-blocking resources, the last to finish first (at most 50)
items: [...blockingSync]
.sort((a, b) => b.responseEnd - a.responseEnd)
.slice(0, 50)
.map((r) => ({
url: r.name.split("/").pop().split("?")[0] || r.name,
type: r.initiatorType === "link" ? "CSS" : "JS",
durationMs: Math.round(r.duration),
})),
};
})();
Phase breakdown
The three phases explain where time is spent before the first pixel is painted:
| Phase | Measured as | Optimization |
|---|---|---|
| TTFB | responseStart, time until first byte of HTML arrives | Server response time, CDN, caching |
| Render-blocking load | Time blocked waiting for CSS/JS before the browser can paint | Defer/async scripts, inline critical CSS |
| Render delay | Time from resources ready to actual paint | CSS complexity, main thread contention |
On prerendered pages, every phase is measured from activationStart (the moment the page was shown), not from the start of the prerender.
Browser support: The render-blocking phase requires the renderBlockingStatus API (Chrome 107+, Edge 107+). In other browsers the render-blocking section will show no resources even if they exist.
Browser support
The snippet needs these features to run.
| Feature | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
| PerformancePaintTiming (opens in a new tab) | 60 | 79 | 84 | 14.1 |
| PerformanceObserver (opens in a new tab) | 52 | 79 | 57 | 11 |
| PerformanceNavigationTiming (opens in a new tab) | 57 | 12 | 58 | 15 |
| All of the above | 60 | 79 | 84 | 15 |
The snippet reports on these features. It runs without them, and its advice on each one applies where it is supported.
| Feature | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
| PerformanceResourceTiming.renderBlockingStatus (opens in a new tab) | 107 | 107 |
Source: MDN browser compatibility data (opens in a new tab), version 8.1.4.
Further reading
- First Contentful Paint (FCP) (opens in a new tab) | web.dev
- Optimize FCP (opens in a new tab) | web.dev
- Find Render-Blocking Resources | Detailed analysis with fixes
- TTFB | Time to First Byte
- LCP | Largest Contentful Paint
- LCP Subparts | Detailed LCP phase breakdown