LCP Sub-Parts
Overview
Breaks down Largest Contentful Paint (opens in a new tab) into its four phases to identify optimization opportunities. Understanding which phase is slowest helps you focus your optimization efforts where they'll have the most impact. Based on the Web Vitals Chrome Extension (opens in a new tab).
Why this matters:
LCP is a critical Core Web Vital that measures when the largest content element becomes visible. By breaking it into sub-parts, you can pinpoint exactly where delays occur and apply targeted optimizations instead of guessing.
The four phases of LCP:
| Sub-part | What it measures | Common causes |
|---|---|---|
| Time to First Byte | Server response time | Slow server, no CDN, no caching |
| Resource Load Delay | Time from TTFB until LCP resource starts loading | Render-blocking resources, late discovery |
| Resource Load Time | Time to download the LCP resource | Large images, slow connection, no compression |
| Element Render Delay | Time from download complete to paint | Client-side rendering, render-blocking JS |
Optimization priority:
| Phase | Target | How to optimize |
|---|---|---|
| TTFB | < 800ms | Use CDN, optimize server, enable caching |
| Load Delay | < 10% of LCP | Preload LCP image, remove render-blocking resources |
| Load Time | < 40% of LCP | Compress images, use modern formats (WebP, AVIF) |
| Render Delay | < 10% of LCP | Inline critical CSS, defer non-critical JS |
Quick check: Use LCP for a simple LCP value and element identification.
Snippet
// LCP Sub-Parts Analysis
// https://webperf-snippets.nucliweb.net
(() => {
const formatMs = (ms) => `${Math.round(ms)}ms`;
const formatPercent = (value, total) => `${Math.round((value / total) * 100)}%`;
const valueToRating = (ms) =>
ms <= 2500 ? "good" : ms <= 4000 ? "needs-improvement" : "poor";
const RATING = {
good: { icon: "🟢", color: "#0CCE6A" },
"needs-improvement": { icon: "🟡", color: "#FFA400" },
poor: { icon: "🔴", color: "#FF4E42" },
};
const SUB_PARTS = [
{ name: "Time to First Byte", key: "ttfb", target: 800 },
{ name: "Resource Load Delay", key: "loadDelay", targetPercent: 10 },
{ name: "Resource Load Time", key: "loadTime", targetPercent: 40 },
{ name: "Element Render Delay", key: "renderDelay", targetPercent: 10 },
];
const getNavigationEntry = () => {
const navEntry = performance.getEntriesByType("navigation")[0];
if (navEntry?.responseStart > 0 && navEntry.responseStart < performance.now()) {
return navEntry;
}
return null;
};
const observer = new PerformanceObserver((list) => {
const lcpEntry = list.getEntries().at(-1);
if (!lcpEntry) return;
const navEntry = getNavigationEntry();
if (!navEntry) return;
const lcpResEntry = performance
.getEntriesByType("resource")
.find((e) => e.name === lcpEntry.url);
const activationStart = navEntry.activationStart || 0;
// Calculate sub-parts
const ttfb = Math.max(0, navEntry.responseStart - activationStart);
const lcpRequestStart = Math.max(
ttfb,
lcpResEntry
? (lcpResEntry.requestStart || lcpResEntry.startTime) - activationStart
: 0
);
const lcpResponseEnd = Math.max(
lcpRequestStart,
lcpResEntry ? lcpResEntry.responseEnd - activationStart : 0
);
const lcpRenderTime = Math.max(
lcpResponseEnd,
lcpEntry.startTime - activationStart
);
const subPartValues = {
ttfb: ttfb,
loadDelay: lcpRequestStart - ttfb,
loadTime: lcpResponseEnd - lcpRequestStart,
renderDelay: lcpRenderTime - lcpResponseEnd,
};
// LCP Rating
const rating = valueToRating(lcpRenderTime);
const { icon, color } = RATING[rating];
console.group(
`%cLCP: ${icon} ${formatMs(lcpRenderTime)} (${rating})`,
`color: ${color}; font-weight: bold; font-size: 14px;`
);
// Element info
if (lcpEntry.element) {
const el = lcpEntry.element;
let selector = el.tagName.toLowerCase();
if (el.id) selector = `#${el.id}`;
else if (el.className && typeof el.className === "string") {
const classes = el.className.trim().split(/\s+/).slice(0, 2).join(".");
if (classes) selector = `${el.tagName.toLowerCase()}.${classes}`;
}
console.log("");
console.log("%cLCP Element:", "font-weight: bold;");
console.log(` ${selector}`, el);
if (lcpEntry.url) {
const shortUrl = lcpEntry.url.split("/").pop()?.split("?")[0] || lcpEntry.url;
console.log(` URL: ${shortUrl}`);
}
// Highlight
el.style.outline = "3px dashed lime";
el.style.outlineOffset = "2px";
}
// Sub-parts table
console.log("");
console.log("%cSub-Parts Breakdown:", "font-weight: bold;");
// Find the slowest phase
const phases = SUB_PARTS.map((part) => ({
...part,
value: subPartValues[part.key],
percent: (subPartValues[part.key] / lcpRenderTime) * 100,
}));
const slowest = phases.reduce((a, b) => (a.value > b.value ? a : b));
const tableData = phases.map((part) => {
const isSlowest = part.key === slowest.key;
const isOverTarget = part.target
? part.value > part.target
: part.percent > part.targetPercent;
return {
"Sub-part": isSlowest ? `⚠️ ${part.name}` : part.name,
Time: formatMs(part.value),
"%": formatPercent(part.value, lcpRenderTime),
Status: isOverTarget ? "🔴 Over target" : "✅ OK",
};
});
console.table(tableData);
// Visual bar
const barWidth = 40;
const bars = phases.map((p) => {
const width = Math.max(1, Math.round((p.value / lcpRenderTime) * barWidth));
return { key: p.key, bar: width };
});
const ttfbBar = "█".repeat(bars[0].bar);
const delayBar = "▓".repeat(bars[1].bar);
const loadBar = "▒".repeat(bars[2].bar);
const renderBar = "░".repeat(bars[3].bar);
console.log("");
console.log(` ${ttfbBar}${delayBar}${loadBar}${renderBar}`);
console.log(" █ TTFB ▓ Load Delay ▒ Load Time ░ Render Delay");
// Recommendations based on slowest phase
console.log("");
console.log("%c💡 Optimization Focus:", "font-weight: bold; color: #3b82f6;");
console.log(` Slowest phase: ${slowest.name} (${formatPercent(slowest.value, lcpRenderTime)})`);
if (slowest.key === "ttfb") {
console.log(" → Use a CDN to reduce latency");
console.log(" → Enable server-side caching");
console.log(" → Optimize server response time");
} else if (slowest.key === "loadDelay") {
console.log(" → Preload the LCP image: <link rel=\"preload\" as=\"image\" href=\"...\">");
console.log(" → Remove render-blocking resources");
console.log(" → Inline critical CSS");
} else if (slowest.key === "loadTime") {
console.log(" → Compress and resize the LCP image");
console.log(" → Use modern formats (WebP, AVIF)");
console.log(" → Use a CDN for faster delivery");
} else if (slowest.key === "renderDelay") {
console.log(" → Reduce render-blocking JavaScript");
console.log(" → Avoid client-side rendering for LCP element");
console.log(" → Use fetchpriority=\"high\" on LCP image");
}
// Performance entries for DevTools
SUB_PARTS.forEach((part) => performance.clearMeasures(part.name));
phases.forEach((part) => {
const startTimes = {
ttfb: 0,
loadDelay: ttfb,
loadTime: lcpRequestStart,
renderDelay: lcpResponseEnd,
};
performance.measure(part.name, {
start: startTimes[part.key],
end: startTimes[part.key] + part.value,
});
});
console.log("");
console.log("%c📊 Measures added to Performance timeline", "color: #666;");
console.log(" Open DevTools → Performance → reload to see waterfall");
console.groupEnd();
});
observer.observe({ type: "largest-contentful-paint", buffered: true });
console.log("%c📊 LCP Sub-Parts Analysis Active", "font-weight: bold; font-size: 14px;");
console.log(" Waiting for LCP...");
})();
Understanding the Results
Sub-Parts Table:
| Column | Description |
|---|---|
| Sub-part | Phase name (⚠️ marks the slowest) |
| Time | Duration in milliseconds |
| % | Percentage of total LCP |
| Status | ✅ OK or 🔴 Over target |
Visual Bar:
Shows time distribution across the four phases:
- █ TTFB (server response)
- ▓ Load Delay (discovery to request)
- ▒ Load Time (download)
- ░ Render Delay (paint)
Performance Timeline:
The snippet adds measures to the Performance API. To see them:
- Open DevTools → Performance tab
- Reload the page
- Look for "Time to First Byte", "Resource Load Delay", etc. in the timeline
Optimization Checklist
| If slowest is... | Check these |
|---|---|
| TTFB | Server response time, CDN, caching headers |
| Load Delay | Preload hints, render-blocking resources, late <img> in HTML |
| Load Time | Image size, format, compression, CDN |
| Render Delay | JavaScript blocking, CSS parsing, client-side rendering |
Optimization Decision Tree:
Further Reading
- Optimize LCP (opens in a new tab) | web.dev
- LCP breakdown (opens in a new tab) | web.dev
- Preload critical assets (opens in a new tab) | web.dev
- LCP Quick Check | Simple LCP measurement