Ringkasan Utama
Cara meningkatkan Core Web Vitals di Next.js: ukur dulu dengan PageSpeed Insights, beri gambar hero priority serta width dan height atau fill, tulis sizes untuk gambar responsif, pakai width dan height tetap untuk gambar kecil, dan muat font lewat next/font. Target baik menurut web.dev: LCP 2,5 detik, INP 200 milidetik, CLS 0,1.
Kamu menjalankan PageSpeed Insights untuk pertama kali dan melihat LCP berwarna merah, padahal halamannya hanya berisi satu gambar besar dan beberapa teks. Kamu menambahkan komponen Image seperti di dokumentasi, tetapi skornya tidak berubah. Atau teks di atas lipatan melompat sesaat setelah font masuk. Pemula biasanya tersangkut di sini karena tidak tahu bagian mana yang lambat, jadi semua perbaikan terasa seperti tebakan.
Panduan ini membenahi tiga hal yang paling sering menjadi penyebab: gambar hero, ukuran gambar responsif, dan font. Semua nama prop dan perilakunya saya cocokkan dengan dokumentasi Next.js 15 dan web.dev. Contohnya mengikuti pola yang dipakai situs ini: hero blog memakai fill, sizes, dan priority, logo kecil memakai width dan height tetap, dan font dihosting sendiri lewat next/font/local. Saya tidak menjanjikan berapa milidetik yang kamu hemat; itu harus kamu ukur di halamanmu sendiri.
Ingin memahami konsepnya dulu dari nol? Ada pelajaran gratis Core Web Vitals, next/image, dan next/font dengan diagram interaktif dan kuis. Artikel ini adalah versi praktik langkah demi langkahnya.
Yang kamu butuhkan
- Project Next.js 15 dengan App Router yang bisa di-build (npm run build).
- Satu halaman dengan gambar besar di bagian atas (hero) dan beberapa gambar kecil seperti logo.
- Browser Chrome dengan DevTools, dan akses ke pagespeed.web.dev untuk situs yang sudah online.
- Kemauan mengukur sebelum dan sesudah setiap perubahan, satu perubahan per kali.
Kalau kamu butuh gambaran performa Next.js yang lebih luas, baca juga panduan optimasi performa Next.js setelah selesai di sini.
Langkah 1: Ukur dulu, bedakan data lapangan dan data lab
Sebelum mengubah kode, ketahui angka awalnya. PageSpeed Insights menampilkan dua jenis data. Data lapangan berasal dari Chrome User Experience Report (CrUX), yaitu kunjungan nyata selama 28 hari terakhir, dan ditampilkan pada persentil ke-75. Data lab berasal dari Lighthouse yang berjalan di lingkungan simulasi. Jika halamanmu belum punya cukup sampel, PageSpeed Insights memakai data tingkat origin, dan kalau itu pun kurang, tidak ada data lapangan yang ditampilkan.
# 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.Tiga metrik dan batas baik menurut web.dev, semuanya dinilai pada persentil ke-75: LCP (Largest Contentful Paint, kecepatan elemen terbesar muncul) 2,5 detik atau kurang, INP (Interaction to Next Paint, respons terhadap klik dan ketukan) 200 milidetik atau kurang, dan CLS (Cumulative Layout Shift, seberapa banyak tata letak bergeser) 0,1 atau kurang. Hasil yang kamu harapkan dari langkah ini: tiga angka awal yang kamu catat, dan kamu tahu mana yang merah.
Lighthouse tidak bisa mengukur INP karena tidak ada interaksi pengguna; ia memakai Total Blocking Time sebagai pengganti di lab. Data lab dan data lapangan bisa berbeda karena kondisinya berbeda. Pakai lab untuk mencari penyebab, pakai lapangan untuk menilai hasil akhir.
Langkah 2: Perbaiki gambar hero dengan priority
LCP biasanya adalah gambar atau blok teks terbesar yang terlihat saat halaman dibuka, dan di banyak halaman itu adalah gambar hero. Komponen Image dari next/image melakukan lazy load secara default. web.dev menyatakan jangan pernah me-lazy-load gambar LCP, karena unduhannya baru dimulai setelah browser memastikan gambar itu ada di viewport. Pada hero, tambahkan 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
/>Untuk hero yang lebarnya mengikuti layar, pakai fill di dalam pembungkus yang punya rasio aspek tetap. Dokumentasi mensyaratkan induk dari gambar fill berposisi relative, fixed, atau absolute. Beginilah hero blog di situs ini dibuat.
// 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).Hasil yang diharapkan: gambar hero ikut diunduh sejak awal dan ruangnya sudah dipesan, sehingga tidak ada lompatan saat gambar muncul. Ingat, width dan height adalah ukuran intrinsik untuk menghitung rasio aspek; ukuran tampilan tetap diatur CSS. Keduanya wajib kecuali gambar diimpor statis atau memakai fill.
Langkah 3: Tulis sizes untuk gambar responsif
Browser tidak tahu selebar apa gambar akan ditampilkan saat membaca HTML, jadi sizes yang memberitahunya. Menurut dokumentasi Next.js, jika sizes hilang browser mengira gambar selebar viewport, yang bisa mengunduh gambar yang tidak perlu besar. Pakai sizes setiap kali gambar memakai fill atau dibuat responsif lewat 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 juga mengubah markup: tanpa sizes, Next.js menghasilkan srcset terbatas (misalnya 1x dan 2x) yang cocok untuk gambar berukuran tetap; dengan sizes, ia menghasilkan srcset lengkap berisi lebar seperti 640w dan 750w. Browser hanya mengunduh satu kandidat yang dipilihnya. Periksa HTML hasil build untuk memastikannya.
# 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.Langkah 4: Pakai width dan height tetap untuk gambar kecil
Kebalikannya juga penting. Marquee sertifikasi di situs ini menampilkan logo 44 piksel. Komentar di kodenya mencatat bahwa fill dengan sizes berbasis piksel mencantumkan setiap lebar terkonfigurasi di srcset, sekitar 1 KB HTML per tile. Perbaikannya adalah ukuran tetap.
// 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} />Aturan sederhananya: gambar fluid seperti hero dan sampul kartu memakai fill dan sizes; gambar berukuran tetap seperti logo dan avatar memakai width dan height. Kalau kamu penasaran bagaimana srcset dan cache gambar bekerja di balik layar, baca bedah internal optimasi gambar next/image.
Langkah 5: Muat font lewat next/font
next/font melakukan self-hosting file font bersama aset statis lainnya, sehingga browser tidak mengirim request ke penyedia font. Untuk font Google, CSS dan file diunduh saat build. Menurut dokumentasi, ini memungkinkan memuat font web tanpa layout shift. Default display adalah swap dan default preload adalah true. Rekomendasi dokumentasi: pakai font variabel.
// 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>
);
}Kalau build di CI tidak boleh bergantung pada jaringan ke penyedia font, simpan file-nya di repo dan pakai next/font/local. Situs ini pindah ke cara itu setelah build Docker di CI gagal mengunduh font dari Google dan seluruh deploy ikut gagal. Satu file font variabel sekarang mencakup bobot 400 sampai 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.Menurut dokumentasi, next/font menyediakan opsi adjustFontFallback (aktif secara default untuk font Google) agar font cadangan disesuaikan dan CLS berkurang. Untuk font lokal, nilainya Arial, Times New Roman, atau false. Jangan matikan kecuali kamu tahu alasannya.
Langkah 6: Verifikasi hasilnya
Build produksi lalu baca HTML yang benar-benar dikirim, bukan kodenya. Hasil yang diharapkan: ada link preload untuk gambar hero, gambar responsif membawa string sizes yang kamu tulis, dan tidak ada request ke penyedia font.
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)Setelah itu jalankan PageSpeed Insights lagi dan bandingkan dengan angka di Langkah 1. Data lab berubah segera, tetapi data lapangan memakai jendela 28 hari, jadi perbaikan baru terlihat penuh setelah beberapa minggu.
Bagaimana dengan INP? next/image dan next/font menyasar pemuatan dan kestabilan. INP menyangkut kecepatan respons setelah klik atau ketukan, dan itu biasanya soal pekerjaan berat di event handler, bukan gambar. Ukur interaksi nyata sebelum menebak.
Error umum dan cara memperbaikinya
Tujuh dari delapan masalah ini adalah kesalahan penerapan yang gampang dikenali; yang kedelapan adalah salah paham cara membaca angka.
| Gejala | Penyebab | Perbaikan |
|---|---|---|
| LCP merah, hero muncul terlambat | Hero memakai default next/image, yaitu lazy load, sehingga unduhan menunggu layout | Tambahkan priority pada gambar hero |
| Setelah menambah priority, LCP tidak membaik | priority dipasang di banyak gambar, padahal web.dev menyebut lebih dari satu atau dua gambar membuatnya tidak membantu | Pasang hanya pada gambar yang menjadi elemen LCP |
| Next.js meminta width dan height, atau tata letak melompat saat gambar muncul | Rasio aspek tidak diketahui, sehingga ruang tidak dipesan | Set width dan height, impor gambar secara statis, atau pakai fill di dalam pembungkus berposisi |
| Gambar fill tidak tampil atau ukurannya aneh | Elemen induk tidak berposisi relative, fixed, atau absolute | Beri induknya position relative dan tinggi atau rasio aspek yang jelas |
| Ponsel mengunduh gambar yang terlalu besar | sizes tidak ditulis, jadi browser mengira gambar selebar viewport | Tulis sizes sesuai lebar tampilan sebenarnya per breakpoint |
| HTML membengkak oleh srcset panjang untuk logo atau ikon kecil | fill dengan sizes piksel mencantumkan setiap lebar terkonfigurasi | Pakai width dan height tetap agar srcset hanya 1x dan 2x |
| Gambar dari domain lain gagal dengan 400 Bad Request | Host gambar belum didaftarkan di remotePatterns pada konfigurasi Next.js | Tambahkan hostname dan path-nya ke images.remotePatterns, lalu restart |
| Lab hijau, tetapi PageSpeed Insights lapangan masih merah | Data lab adalah simulasi; data lapangan adalah pengguna nyata selama 28 hari pada persentil ke-75 | Nilai hasil dari data lapangan, pakai lab untuk debugging, dan tunggu jendela 28 hari |
Checklist sebelum deploy
- Kamu sudah mencatat LCP, INP, dan CLS awal dari PageSpeed Insights.
- Gambar hero punya priority, dan hanya satu atau dua gambar yang punya.
- Setiap gambar punya width dan height, atau fill di dalam pembungkus berposisi.
- Gambar fluid punya sizes; gambar kecil berukuran tetap tidak memakai fill.
- Font dimuat lewat next/font, sekali di satu modul, dengan subsets atau file lokal.
- HTML hasil build diperiksa dengan curl, lalu PageSpeed Insights dijalankan ulang.
Belajar lebih dalam
Artikel ini fokus pada langkah praktik. Penjelasan konsepnya, termasuk empat subbagian LCP dan diagram jalur satu gambar dari HTML sampai tampil, ada di pelajaran gratis berikut.
Pelajaran: dasar Core Web Vitals, next/image, dan next/font
Aturan yang bisa kamu bawa: ukur dulu, perbaiki elemen LCP dengan priority, jelaskan ukuran gambar lewat width, height, atau sizes, dan biarkan next/font menghosting font. Satu perubahan, satu pengukuran.

