Largest Contentful Paint in the poor range
What is this issue?
Largest Contentful Paint (LCP) measures how long it takes for the biggest thing on the screen -- usually a hero image, a video poster or a large block of text -- to finish rendering after somebody opens the page. It is the metric that answers "when did this page feel loaded?".
Google puts every page into one of three bands:
| LCP | Verdict |
|---|---|
| 2.5 seconds or less | Good |
| 2.5 to 4 seconds | Needs improvement |
| more than 4 seconds | Poor |
This check reports pages in the poor band.
LCP is one of the three Core Web Vitals, and it is the one a visitor notices first: on a phone over a mobile connection, four seconds of blank space is long enough to go back to the search results.
For a page to pass, its largest visible element should render within 2.5 seconds.
Why it matters
- It is a ranking signal. Core Web Vitals feed Google's page-experience signals. It is not the strongest input to a ranking, but it is one of very few Google publishes thresholds for and reports back to you in Search Console.
- It decides whether the visit happens at all. A visitor who leaves before the main content appears never sees the content you ranked for, and the abandonment is attributed to the page rather than to the server that was slow.
- It compounds. A page slow to first paint is usually slow at every subsequent interaction too, so LCP is often the first visible symptom of a problem that is also costing conversions.
- Everyone is measured on the same scale. These are Google's numbers, not ours; the same figure appears in PageSpeed Insights and in Search Console.
Fixing it moves the page out of the poor band and removes an important-severity finding from the site's health score.
How to fix it
Find out what the LCP element actually is. It is almost always one of: a hero image, a background image, a video poster, or the first large heading. Optimising the wrong element changes nothing.
Cut the server's response time first. Nothing can render before the HTML arrives. If "Server takes too long to send the first byte" is also failing, that is the cause and it comes first.
Let the browser discover the LCP image early. Reference it in the HTML rather than loading it from JavaScript or a CSS background, and never mark it
loading="lazy"-- lazy-loading the element the metric measures is the single most common cause of a poor LCP.Make the image smaller. Serve a modern format, size it for the viewport it will be displayed at, and compress it. A hero photo sized for a desktop screen and sent to a phone is a large share of the delay.
Stop render-blocking resources queueing in front of it. Inline the styles needed for the first screen and defer the rest; defer scripts not needed to paint.
Preconnect to the origin the image comes from when it is on a CDN or another domain, so the connection is already open when the request is made.
Cache the HTML. A page rendered per request cannot be fast for the first visitor; a page served from an edge cache can.
Examples
1. The hero image was lazy-loaded
A product page put loading="lazy" on every image site-wide, including the hero, so
the browser waited until layout was complete before requesting the one image the
metric measures.
Fails -- LCP 5.1 s
<img src="/hero-2400.jpg" loading="lazy" alt="The product on a desk">Passes -- LCP 1.9 s
<img src="/hero-1200.avif" fetchpriority="high" alt="The product on a desk"
width="1200" height="675">Lazy-loading is right for images below the fold and wrong for the one at the top.
2. The image was only discoverable from CSS
The hero was a CSS background, so the browser could not request it until the stylesheet had been downloaded and parsed.
Fails -- LCP 4.4 s
.hero { background-image: url(/hero.jpg); }Passes -- LCP 2.2 s
<img class="hero" src="/hero.avif" alt="" width="1600" height="900">An <img> in the HTML is found by the browser's preload scanner immediately.
3. A slow origin, not a slow page
The markup was small and the images well optimised, but the HTML itself took 2.6 seconds to arrive because every request re-rendered the page from the database.
Fails -- TTFB 2.6 s, LCP 4.8 s
Passes -- the same page behind an edge cache: TTFB 180 ms, LCP 2.1 s
Nothing on the page changed. When TTFB is also failing it is the cause, not a second problem.
How PixyScan detects this
Chooses which pages to measure. After the crawl, PixyScan measures the homepage plus the pages your own site links to most, up to a per-scan budget (ten pages by default). Measuring every page would take hours and exhaust the quota of the free API this uses.
Asks the PageSpeed Insights API about each of them, on mobile.
Prefers real-user data. Where the Chrome UX Report has enough traffic for that exact URL, LCP is the 75th percentile of what real Chrome users experienced over the last 28 days. Where it does not, the LCP from the PageSpeed lab run is used instead, and the report says which of the two it used.
Ignores origin-level data for a page verdict. When the report has too little data for the URL, PageSpeed answers with the whole site's distribution instead. That is a statement about the site, not about this page, so it is not used here.
Raises the issue only above 4 seconds -- Google's own "poor" bound. A page at exactly 4 seconds is "needs improvement" and raises nothing.
Raises nothing on a page it did not measure. A page outside the sample, and a page whose measurement failed, are both reported as
Not measured. Neither is treated as passing, and neither can fail this check.
What we store
Storage Level
Page Level
Database Table / Prisma Model
PagePerformance (page_performance), with the finding itself on
audit_issues.details.
Fields Used
| Field | Type | Description |
|---|---|---|
| strategy | String | mobile or desktop. Every number in the row belongs to one form factor. |
| fetchedAt | DateTime | When PageSpeed answered. "Measured three weeks ago" is not "not measured". |
| errorReason | String? | Why a sampled page still has no numbers. Null when the call succeeded. |
| performanceScore | Int? | Lighthouse's performance category, rescaled 0-100. |
| labLcpMs | Int? | LCP from the PageSpeed lab run, in milliseconds. |
| fieldLcpMs | Int? | LCP from the Chrome UX Report for this URL. Null when the report has no data for it. |
On the finding (audit_issues.details): metric, source (field or lab),
valueMs and thresholdMs (4000).
Detection Dependencies
- PageSpeed Insights API v5 (
lighthouseResult) - Chrome UX Report field data (
loadingExperience), where the URL has the traffic - The crawl's internal link graph, which decides which pages are sampled
Note
A page with no row was never measured. The pass samples the homepage plus the
most-linked pages within a per-scan budget (PERFORMANCE_SAMPLE_LIMIT, default 10),
so most pages of a site have no page_performance row at all. That is the normal
state, it is drawn as Not measured, and this check cannot be raised against it.
A row whose metric columns are all null with an errorReason set is a different
state again: the page WAS sampled and the measurement failed. It is still never
reported as passing.