Key Takeaway
To improve Core Web Vitals in Next.js: measure first with PageSpeed Insights, give the hero image priority plus width and height or fill, write sizes for responsive images, use fixed width and height for small images, and load fonts through next/font. web.dev's good targets are LCP 2.5 seconds, INP 200 milliseconds, CLS 0.1.
You run PageSpeed Insights for the first time and LCP is red, even though the page is one big image and a little text. You add the Image component the way the docs show, and the score does not move. Or the text above the fold jumps a moment after the font arrives. Beginners get stuck here because they cannot tell which part is slow, so every fix feels like a guess.
This guide fixes the three most common causes: the hero image, responsive image sizing, and fonts. Every prop name and behaviour was checked against the Next.js 15 docs and web.dev. The examples follow what this site does: the blog hero uses fill, sizes and priority, small logos use fixed width and height, and the font is self-hosted through next/font/local. I do not promise how many milliseconds you will save; you have to measure that on your own pages.
Want the concepts from scratch first? There is a free lesson on Core Web Vitals, next/image and next/font with an interactive diagram and a quiz. This article is the hands-on, step by step version.
What you need
- A Next.js 15 project using the App Router that builds (npm run build).
- One page with a large image at the top (the hero) and a few small images such as logos.
- Chrome with DevTools, and access to pagespeed.web.dev for a site that is online.
- The discipline to measure before and after each change, one change at a time.
For a wider view of Next.js performance, also read the Next.js performance optimization guide once you finish here.
Step 1: Measure first, and tell field data from lab data
Before changing code, know your starting numbers. PageSpeed Insights shows two kinds of data. Field data comes from the Chrome User Experience Report (CrUX), real visits over the previous 28 days, shown at the 75th percentile. Lab data comes from Lighthouse running in a simulated environment. If your page lacks enough samples, PageSpeed Insights falls back to origin-level data, and if that is also thin, no field data is shown.
# Ukur build produksi, bukan npm run dev
npm run build && npm start
# Situs yang sudah online: tempel URL halamanmu di
# https://pagespeed.web.dev/
# Baca dua bagian: data lapangan (field) di atas, data lab (Lighthouse) di bawah.The three metrics and web.dev's good thresholds, all judged at the 75th percentile: LCP (Largest Contentful Paint, how fast the largest element appears) 2.5 seconds or less, INP (Interaction to Next Paint, how fast the page responds to clicks and taps) 200 milliseconds or less, and CLS (Cumulative Layout Shift, how much the layout moves) 0.1 or less. What to expect from this step: three starting numbers written down, and you know which one is red.
Lighthouse cannot measure INP because there is no user input; it uses Total Blocking Time as a lab stand-in. Lab and field data can disagree because they measure different conditions. Use lab to find causes and field to judge the result.
Step 2: Fix the hero image with priority
LCP is usually the largest image or text block visible when the page opens, and on many pages that is the hero image. The Image component from next/image lazy loads by default. web.dev says never to lazy-load the LCP image, because the download only starts once the browser confirms the image is in the viewport. On the hero, add priority.
import Image from "next/image";
// Wrong: hero ini elemen LCP, tetapi next/image lazy load secara default,
// jadi unduhannya baru mulai setelah layout selesai.
<Image src="/hero.jpg" alt="Meja kerja" width={1200} height={630} />
// Right: priority = preload gambar + matikan lazy loading.
// width dan height memberi rasio aspek, jadi ruangnya sudah dipesan (CLS aman).
<Image
src="/hero.jpg"
alt="Meja kerja"
width={1200}
height={630}
priority
/>For a hero that spans the screen, use fill inside a wrapper with a fixed aspect ratio. The docs require the parent of a fill image to be positioned relative, fixed or absolute. This is how this site's blog hero is built.
// Gambar fluid: fill + pembungkus berposisi + sizes.
// Pembungkus punya rasio aspek tetap, jadi kotaknya ada sebelum gambarnya.
<div className="relative aspect-[1200/630] w-full">
<Image
src="/hero.jpg"
alt="Meja kerja"
fill
sizes="(max-width: 768px) calc(100vw - 2rem), 768px"
className="object-cover"
priority
/>
</div>
// Wrong: fill tanpa pembungkus berposisi (relative, fixed, atau absolute).What to expect: the hero image is fetched early and its space is already reserved, so nothing jumps when it appears. Remember that width and height are the intrinsic size used to compute the aspect ratio; the displayed size is still set by CSS. You must set both unless the image is statically imported or uses fill.
Step 3: Write sizes for responsive images
The browser does not know how wide an image will be shown when it reads the HTML, so sizes tells it. Per the Next.js docs, if sizes is missing the browser assumes the image is as wide as the viewport, which can download an unnecessarily large image. Use sizes whenever an image uses fill or is made responsive with CSS.
// Wrong: tanpa sizes, browser mengira gambar selebar layar (100vw),
// jadi ponsel bisa mengunduh file yang jauh lebih besar dari kartunya.
<Image src={post.cover} alt={post.title} fill />
// Right: sizes menjelaskan lebar tampilan yang sebenarnya per breakpoint.
<Image
src={post.cover}
alt={post.title}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>sizes also changes the markup: without it, Next.js generates a limited srcset (for example 1x and 2x) that suits fixed-size images; with it, Next.js generates a full srcset of widths such as 640w and 750w. The browser downloads only the one candidate it picks. Check the built HTML to confirm.
# Gambar ber-fill harus menampilkan string sizes yang kamu tulis
curl -s http://localhost:3000/ | grep -o 'sizes="[^"]*"' | head
# Dengan sizes, srcset berisi daftar lebar (kira-kira seperti ini):
# srcset="... 640w, ... 750w, ... 828w, ... 1080w, ..."
# Tanpa sizes, srcset hanya berisi 1x dan 2x.Step 4: Use fixed width and height for small images
The reverse matters too. The certification marquee on this site shows 44 pixel logos. A comment in its code records that fill with a pixel-based sizes lists every configured width in srcset, about 1 KB of HTML per tile. The fix was fixed dimensions.
// Wrong: logo 44 px memakai fill + sizes piksel. Setiap lebar terkonfigurasi
// masuk ke srcset untuk TIAP logo, sekitar 1 KB HTML per tile di situs ini.
<div className="relative h-11 w-11">
<Image src={logo} alt="" fill sizes="44px" />
</div>
// Right: ukuran tetap = width dan height saja. srcset hanya 1x dan 2x.
<Image src={logo} alt="" width={44} height={44} />The simple rule: fluid images such as heroes and card covers use fill and sizes; fixed-size images such as logos and avatars use width and height. If you are curious how srcset and the image cache work behind the scenes, read the internals of next/image optimization.
Step 5: Load fonts through next/font
next/font self-hosts font files with the rest of your static assets, so the browser sends no requests to a font provider. For Google fonts, the CSS and files are downloaded at build time. According to the docs, this lets you load web fonts with no layout shift. The default display is swap and the default preload is true. The docs recommend variable fonts.
// app/fonts.ts: muat font SEKALI di satu modul
import { Inter } from "next/font/google";
// Font variabel: tidak perlu menulis weight
export const inter = Inter({
subsets: ["latin"], // tanpa subsets, preload memberi peringatan
display: "swap",
variable: "--font-inter",
});
// app/layout.tsx
import { inter } from "./fonts";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="id" className={inter.variable}>
<body>{children}</body>
</html>
);
}If your CI build must not depend on the network to a font provider, keep the file in the repo and use next/font/local. This site switched to that after the Docker build in CI failed to download the font from Google and the whole deploy failed with it. One variable font file now covers weights 400 to 800.
// Alternatif: file font ada di repo, build tidak butuh jaringan
import localFont from "next/font/local";
export const myFont = localFont({
src: "./my-font.woff2", // relatif terhadap file yang memanggil localFont
weight: "400 800", // rentang bobot untuk font variabel
display: "swap",
variable: "--font-my",
});
// Wrong: memanggil Inter() atau localFont() di banyak file.
// Tiap pemanggilan menghosting satu instance font lagi.Per the docs, next/font has an adjustFontFallback option (on by default for Google fonts) that adjusts the fallback font to reduce CLS. For local fonts the value is Arial, Times New Roman or false. Do not turn it off unless you know why.
Step 6: Verify the result
Build for production and read the HTML that is really shipped, not the source. What to expect: a preload link for the hero image, responsive images carrying the sizes string you wrote, and no request to a font provider.
npm run build && npm start
# 1. Hero harus punya link preload di head
curl -s http://localhost:3000/ | grep -o 'rel="preload"[^>]*' | head
# 2. Gambar responsif harus membawa sizes yang kamu tulis
curl -s http://localhost:3000/ | grep -o 'sizes="[^"]*"' | head
# 3. Tidak boleh ada request ke penyedia font saat halaman dibuka
# (cek tab Network di DevTools; font dilayani dari domainmu sendiri)Then run PageSpeed Insights again and compare with the numbers from Step 1. Lab data changes immediately, but field data uses a 28 day window, so an improvement only shows fully after a few weeks.
What about INP? next/image and next/font target loading and stability. INP is about how fast the page responds after a click or tap, and that is usually heavy work in an event handler, not images. Measure real interactions before guessing.
Common errors and how to fix them
Seven of these eight problems are easy-to-spot implementation mistakes; the eighth is a misreading of the numbers.
| Symptom | Cause | Fix |
|---|---|---|
| LCP is red and the hero appears late | The hero uses the next/image default, lazy loading, so the download waits for layout | Add priority to the hero image |
| LCP did not improve after adding priority | priority is on many images, and web.dev says more than one or two stops it helping | Set it only on the image that is the LCP element |
| Next.js asks for width and height, or the layout jumps when the image appears | The aspect ratio is unknown, so no space is reserved | Set width and height, import the image statically, or use fill inside a positioned wrapper |
| A fill image does not show or has a strange size | The parent element is not positioned relative, fixed or absolute | Give the parent position relative and a clear height or aspect ratio |
| Phones download an image that is too large | sizes is missing, so the browser assumes the image is as wide as the viewport | Write sizes to match the real displayed width at each breakpoint |
| HTML bloats with a long srcset for small logos or icons | fill with a pixel sizes lists every configured width | Use fixed width and height so srcset is only 1x and 2x |
| An image from another domain fails with 400 Bad Request | The image host is not registered in remotePatterns in the Next.js config | Add the hostname and path to images.remotePatterns, then restart |
| Lab is green but field data in PageSpeed Insights is still red | Lab data is a simulation; field data is real users over 28 days at the 75th percentile | Judge results by field data, use lab for debugging, and wait out the 28 day window |
Checklist before you deploy
- You wrote down the starting LCP, INP and CLS from PageSpeed Insights.
- The hero image has priority, and only one or two images do.
- Every image has width and height, or fill inside a positioned wrapper.
- Fluid images have sizes; small fixed-size images do not use fill.
- Fonts load through next/font, once in one module, with subsets or a local file.
- You checked the built HTML with curl, then re-ran PageSpeed Insights.
Go deeper
This article focuses on the practical steps. The concepts, including the four LCP subparts and a diagram of one image from HTML to paint, are in the free lesson below.
Lesson: Core Web Vitals, next/image and next/font basics
The rule to carry: measure first, fix the LCP element with priority, describe image size through width, height or sizes, and let next/font host your fonts. One change, one measurement.

