Prerequisites
- Access to field RUM (CrUX, Search Console, or your own real-user metrics) and a lab trace tool capable of recording navigation and resource timing for a phone-sized viewport.
Implementation decision
- Diagnose by splitting LCP into four actionable subparts: TTFB, resource discovery delay, resource download duration, and element render delay. Use navigationStart as the baseline for TTFB. Reproduce the same phone-sized viewport and content state when you collect the lab trace so the subpart distribution matches the real complaint.
Expected result
- You’ll identify which subpart dominates the visible delay on phones and get a focused recommendation (for example: cache HTML or move backend work for TTFB; preload or move markup earlier for discovery delay; compress or change format for download duration; inline critical CSS or remove blocking scripts for render delay). A targeted fix should change the dominant subpart in lab traces and — when rolled out — surface in field metrics for the tested page.
One limitation
- The workflow proves impact only for the precise page, viewport, and content shape you tested. Different pages or hero content can surface different dominant subparts, so repeat the method per page type rather than generalizing.
The server will insert a short verified implementation example that computes the breakdown and selects a recommendation for the dominant subpart.
Working example
export function analyzeLCP({ navigationTiming, lcpEntry, resourceTiming } = {}) {
if (!navigationTiming || typeof navigationTiming.responseStart !== 'number') {
throw new TypeError('navigationTiming with numeric responseStart required');
}
if (typeof lcpEntry?.renderTime !== 'number') {
throw new TypeError('lcpEntry with numeric renderTime required');
}
const navigationStart = typeof navigationTiming.navigationStart === 'number'
? navigationTiming.navigationStart
: 0;
const ttfb = Math.max(0, navigationTiming.responseStart - navigationStart);
const hasResource = Boolean(
resourceTiming &&
typeof resourceTiming.startTime === 'number' &&
typeof resourceTiming.responseEnd === 'number'
);
let resourceLoadDelay = 0;
let resourceLoadDuration = 0;
let elementRenderDelay = 0;
if (hasResource) {
resourceLoadDelay = Math.max(0, resourceTiming.startTime - navigationTiming.responseStart);
resourceLoadDuration = Math.max(0, resourceTiming.responseEnd - resourceTiming.startTime);
elementRenderDelay = Math.max(0, lcpEntry.renderTime - resourceTiming.responseEnd);
} else {
elementRenderDelay = Math.max(0, lcpEntry.renderTime - navigationTiming.responseStart);
}
const totalLcpMs = ttfb + resourceLoadDelay + resourceLoadDuration + elementRenderDelay;
const parts = [
{ name: 'ttfb', ms: ttfb },
{ name: 'resourceLoadDelay', ms: resourceLoadDelay },
{ name: 'resourceLoadDuration', ms: resourceLoadDuration },
{ name: 'elementRenderDelay', ms: elementRenderDelay },
];
// When two parts tie, prefer the part that better maps to a developer-actionable
// front-end intervention. In practice we want elementRenderDelay to win ties
// with ttfb so that zero-resource LCPs attribute dominance to rendering.
const priority = {
ttfb: 1,
resourceLoadDelay: 2,
resourceLoadDuration: 3,
elementRenderDelay: 4,
};
const dominant = parts.reduce((a, b) => {
if (a.ms > b.ms) return a;
if (b.ms > a.ms) return b;
// tie: choose higher priority
return priority[a.name] >= priority[b.name] ? a : b;
});
const recommendations = {
ttfb: 'Improve server TTFB: reduce backend work, cache HTML, or use a CDN.',
resourceLoadDelay: 'Start loading the LCP resource earlier: improve discovery or preload it.',
resourceLoadDuration: 'Reduce the resource size or use a more efficient format.',
elementRenderDelay: 'Reduce render-blocking CSS/JS and avoid hiding the LCP element.',
};
return {
totalLcpMs,
breakdown: parts,
dominantSubpart: dominant.name,
recommendation: recommendations[dominant.name],
};
}
Read the full article: https://chriseugenerodriguez.com/blog/diagnose-slow-lcp-on-phones