A client brought in an e-commerce storefront built on Next.js 16 and React 19, backed by a Laravel 12 API. The site felt fast during local development, but field data from the Chrome User Experience Report (CrUX) showed a failing Largest Contentful Paint (LCP) of 3.4 seconds and a Cumulative Layout Shift (CLS) score of 0.22 on mobile. They were losing rankings and dropping ad conversions.
You can't fix Core Web Vitals issues by guessing. Adding random priority props to images or throwing min-height CSS rules at containers without inspecting actual execution frames usually makes performance worse. Here is how to diagnose these issues in real performance traces and fix them directly in Next.js.
Reading the Performance Trace in Chrome DevTools
Forget synthetic score numbers in Lighthouse. To fix real-world problems, open Chrome DevTools, select the Performance tab, check Screenshots and Web Vitals, set CPU throttling to 4x slowdown, and record a page load.
Look at the Timings track. DevTools highlights your LCP element with a distinct marker. Click it, then expand the Summary tab at the bottom. DevTools breaks down LCP into four phases:
- Time to First Byte (TTFB): The time spent waiting for the server to send the first HTML byte.
- Load Delay: The gap between TTFB and when the browser starts fetching the LCP asset.
- Load Duration: The actual time it takes to download the LCP resource.
- Render Delay: The delay between resource download completion and the frame paint.
In our client's trace, Load Delay was eating 1.8 seconds. The server returned the initial server-side rendered (SSR) document in 180ms, but the browser didn't discover the hero image URL until after Next.js hydrated the page bundle and evaluated dynamic React components. The image tag was hidden behind client-side state execution.
Fixing LCP: Preloading and Hero Image Priority
Next.js 16 uses next/image to compress, format, and resize images automatically. However, by default, next/image applies loading="lazy". If your hero banner uses default image settings, you intentionally tell the browser to delay loading the largest visual element on your page until layout calculation finishes.
To fix Load Delay, you must mark the hero image with the priority prop. This disables lazy loading, automatically injects a <link rel="preload" fetchpriority="high"> tag into the document <head>, and lets the browser parse the image request directly from the initial HTML stream before JS bundle execution.
Here is an implementation of a hero component in Next.js 16 connecting to a Laravel 12 backend API:
import Image from 'next/image';
interface ProductHeroProps {
title: string;
imageUrl: string;
altText: string;
}
export default async function ProductHero({ title, imageUrl, altText }: ProductHeroProps) {
return (
<section className="hero-container">
<h1 className="hero-title">{title}</h1>
<div className="hero-image-wrapper">
<Image
src={imageUrl}
alt={altText}
fill
priority
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
style={{ objectFit: 'cover' }}
/>
</div>
</section>
);
}The sizes attribute is mandatory here. Without it, the browser doesn't know how wide the image will render on mobile versus desktop viewports. It defaults to assuming a 100vw layout width, forcing high-density mobile displays to fetch massive 3840px source images, which skyrockets the Load Duration phase.
Adding priority and correct sizes reduced the LCP Load Delay in our trace from 1.8 seconds down to 40ms. The total LCP dropped from 3.4 seconds to 1.1 seconds.
Eliminating CLS: Reserving Space and Managing Fonts
Layout shifts occur when visible DOM elements change position because content above them rendered late or changed size. In Next.js applications, CLS usually comes from three places:
- Images without explicitly reserved layout dimensions.
- Web fonts swapping late and shifting text container line heights.
- Dynamic content blocks rendering asynchronously on the client.
1. Reserving Container Aspect Ratios
When using fill on a next/image component, the parent container must establish a layout box before the image finishes downloading. If the CSS wrapper has no explicit height or aspect ratio, its height defaults to 0 pixels. When the image asset renders, the container expands instantly, pushing all content beneath it down the page.
Match your image dimensions using pure CSS aspect ratio rules inside your stylesheets or standard utility classes:
.hero-image-wrapper {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
background-color: #f3f4f6;
overflow: hidden;
}Setting a background color on the reserved wrapper gives users an immediate visual placeholder and prevents layout shifts completely. The page layout calculates perfectly during the first layout pass, keeping CLS at 0.00.
2. Standardizing Fonts with next/font
Custom self-hosted or Google fonts cause shifts if the fallback font has different kerning or line-height metrics. Next.js 16 provides zero-layout-shift font optimization through next/font. It automatically inlines font CSS at build time and uses the CSS size-adjust property to normalize fallback font metrics with your custom font file.
Set up fonts in your root layout using next/font/google or custom local font imports:
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={inter.variable}>
<body className="font-sans antialiased">{children}</body>
</html>
);
}Because next/font calculates exact fallback proportions, text rendering doesn't alter surrounding element bounds when custom font files finish downloading over slower networks.
The Gotcha: Unoptimized Inline CSS Images
A common trap during optimization audits involves background images set directly via inline CSS style tags: style={{ backgroundImage: `url(${banner})` }}.
Next.js cannot optimize inline CSS background images. They skip format conversion (AVIF/WebP), ignore image dimensions, bypass priority preloading, and cannot use the sizes attribute. In trace analysis, inline background images show up as un-preloaded network fetches that delay LCP significantly. Always replace CSS background images with a structured container using next/image with fill and CSS object-fit: cover.
Verifying Results in Production
Synthetic Lighthouse audits on local developer builds run on powerful hardware with fast connections. They do not reflect real-world network drops or low-end mobile CPUs. Always measure real user performance field metrics.
Track field data directly in your Next.js application using the useReportWebVitals hook, or report them to your Laravel backend endpoint:
'use client';
import { useReportWebVitals } from 'next/web-vitals';
export function WebVitalsReporter() {
useReportWebVitals((metric) => {
if (metric.name === 'LCP' || metric.name === 'CLS') {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
id: metric.id,
page: window.location.pathname,
});
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/v1/telemetry/vitals', body);
} else {
fetch('/api/v1/telemetry/vitals', {
body,
method: 'POST',
keepalive: true,
headers: { 'Content-Type': 'application/json' },
});
}
}
});
return null;
}In our deployment, preloading hero assets, setting explicit aspect ratios on containers, and handling fallback font metrics brought LCP down from 3.4 seconds to 890ms and lowered CLS from 0.22 to 0.001 across all mobile traffic.











