Skip to content
5 min read

How to fix Largest Contentful Paint in Next.js

LCP is usually one element and one slow step. How to find both in Next.js: the image, the font, the server response, and the render path that delays them.

On this page
  1. LCP is one element, not a page score
  2. Break the number into four parts
  3. Time to first byte: render statically wherever you can
  4. Resource load delay: stop hiding the LCP image
  5. Resource load time: serve the right bytes
  6. Render delay: the part everyone misses
  7. Verify against field data, not a lab score
  8. The takeaway

Largest Contentful Paint is the Core Web Vital people most often try to fix by guessing. They compress every image, defer every script, and the number barely moves — because LCP measures one element, and the work went somewhere else.

This is how to find that element, work out which part of its journey is slow, and fix that part specifically, in a Next.js App Router app.

LCP is one element, not a page score

LCP is the render time of the largest content block visible in the viewport: usually a hero image, a large heading, or the first paragraph of text. Not the whole page, not the last thing to load.

Find it before changing anything. PageSpeed Insights names the element directly. In Chrome DevTools, the Performance panel marks it with an LCP badge on the timeline, and hovering it highlights the node.

If the LCP element turns out to be a heading, every minute spent on image formats is wasted. That sounds obvious and it is the single most common failure in LCP work.

Break the number into four parts

Google splits LCP into four sub-parts, and the fix is completely different for each:

Sub-part What it is Typical cause
Time to first byte Server response Dynamic rendering, slow upstream, cold start
Resource load delay Gap between first byte and fetch starting Discovered late, blocked, lazy-loaded
Resource load time The download itself Oversized image, no modern format
Render delay Downloaded but not yet painted Blocking CSS, fonts, hydration

Measure before choosing. If TTFB is 1.8s, image optimisation is irrelevant. If render delay dominates, the file size is not your problem.

Time to first byte: render statically wherever you can

The fastest response is one that was rendered before the request arrived. In the App Router, a route is static unless something opts it out — reading cookies() or headers(), using searchParams, or setting cache: "no-store" on a fetch.

app/blog/[slug]/page.tsxts
export const revalidate = 3600;          // ISR: static HTML, refreshed hourly

export function generateStaticParams() {  // prerender every post at build time
  return getAllPosts().map((p) => ({ slug: p.slug }));
}

Check what you actually shipped: next build prints a symbol per route. ○ is static, ● is prerendered with params, ƒ is rendered per request. A ƒ on a content page is usually an accident — one cookies() call deep in a shared component is enough to opt the whole route out. The caching rules behind this are worth understanding in full, which I covered in Next.js App Router caching.

Resource load delay: stop hiding the LCP image

next/image lazy-loads by default. That is correct for everything below the fold and wrong for the LCP element, because the browser will not fetch it until layout says it is near the viewport — after CSS, after layout, long after it could have started.

tsx
import Image from "next/image";
import hero from "../public/hero.webp";

<Image
  src={hero}
  alt="…"
  priority           // preloads, and disables lazy loading
  sizes="(min-width: 1024px) 50vw, 100vw"
  className="w-full"
/>

priority does two things: it emits a <link rel="preload"> in the head so the fetch starts during HTML parsing, and it turns off lazy loading. Use it on exactly one image per page. Marking several defeats the purpose — preloading everything prioritises nothing.

A static import, as above, gives Next the intrinsic dimensions at build time, which matters for the next section too.

Resource load time: serve the right bytes

Once the fetch starts promptly, size is what remains:

  • Let next/image re-encode. It serves AVIF or WebP by format negotiation. A 650 KB PNG re-encodes to tens of kilobytes with no visible difference.
  • Set sizes honestly. Without it the browser assumes 100vw and downloads a desktop-width image for a 390px phone. With it, it picks from the generated srcset.
  • Check the quality actually served. In Next 16, quality on the component is ignored unless the value is listed in images.qualities in next.config.ts — the URL silently falls back to the default. It is one of several Next.js 16 changes that bite quietly. Read the rendered srcset and confirm the q= parameter is what you set.

Render delay: the part everyone misses

This is where LCP work usually stalls. The element has arrived and still is not painted.

Fonts. If the LCP element is text, a web font blocks it. next/font self-hosts and preloads, removing a third-party round trip, and display: "swap" paints in a fallback immediately rather than holding text invisible:

ts
import { Inter } from "next/font/google";
export const inter = Inter({ subsets: ["latin"], display: "swap" });

Hydration. If the LCP element lives inside a "use client" component that renders nothing until mounted, paint waits for JavaScript. Server Components render to HTML with no client JS at all — keep the hero on the server and push "use client" down to the interactive leaves. The general rule and its cost are in my Next.js performance guide.

Blocking CSS and scripts. A stylesheet in the head blocks rendering by design. Third-party scripts should not be in that path at all — next/script with strategy="afterInteractive" or "lazyOnload" moves them out of it. The same mechanics apply to first paint, which I worked through in how I cut First Contentful Paint.

Verify against field data, not a lab score

A Lighthouse run is a single load on one machine at one network speed. It is a useful experiment, not a measurement of your users.

Use the lab to test a change — one lever, re-run, compare — and the field to decide whether it mattered. Search Console's Core Web Vitals report and the Chrome UX Report give you the 75th percentile of real visits, which is what Google actually assesses. The two will disagree, and the field data wins.

The takeaway

LCP work succeeds when it is narrow: identify the element, split the number into four parts, and fix the part that dominates.

In Next.js that usually means three specific things — render the route statically so the HTML arrives fast, mark exactly one image priority so it is discovered immediately, and keep the hero out of any client component that would make paint wait for hydration. Everything else is tuning, and tuning the wrong sub-part is how a week disappears with the number unchanged.

Frequently asked

What is a good Largest Contentful Paint score?

Google's threshold is 2.5 seconds or less at the 75th percentile of real users, measured on mobile. Between 2.5 and 4 seconds needs improvement, and above 4 seconds is poor. The 75th percentile matters because it means a quarter of your visitors can be slower than your target while the page still passes.

Which element is my LCP element?

Whichever block of content is largest in the viewport when loading finishes — usually a hero image, a heading, or a block of text. Chrome DevTools marks it in the Performance panel, and PageSpeed Insights names it directly. Identify it before optimising, because work done on any other element will not move the number.

Why is my LCP slow when the image is small?

Because LCP is not only download time. It includes the server response, the delay before the browser discovers the resource, and the render delay after it arrives. A small image that is discovered late, blocked by a stylesheet, or rendered by a client component that must hydrate first can produce a worse LCP than a large image loaded immediately.

Does next/image improve LCP automatically?

Partly. It serves modern formats and correct sizes, which cuts download time, but it lazy-loads by default. On the LCP element that is actively harmful, because the browser defers the fetch. Set priority on the one above-the-fold image so it is preloaded and never lazy-loaded.

Found this useful?Share on XShare on LinkedIn

Related reading