Memahami FCP dan LCP: Masalah yang Sering Muncul dan Cara Memperbaikinya
Pembahasan mendalam tentang First Contentful Paint dan Largest Contentful Paint: apa yang diukur, cara mendiagnosisnya, masalah yang membuatnya lambat, dan perbaikan yang benar-benar menggeser angkanya.
Pengunjung menilai situsmu dalam beberapa detik pertama, jauh sebelum mereka mengklik apa pun. Ada dua metrik yang paling tepat menggambarkan detik-detik itu: First Contentful Paint (FCP), saat sesuatu mulai muncul, dan Largest Contentful Paint (LCP), saat konten utama muncul. Kalau salah satunya lambat, halaman terasa rusak, meskipun secara teknis semuanya berjalan.
Artikel ini menjelaskan apa yang diukur oleh masing-masing metrik, cara mencari tahu kenapa angkanya lambat, dan cara memperbaiki masalah yang paling sering terjadi. Contohnya memakai HTML biasa dan Next.js, tetapi konsepnya berlaku untuk stack apa pun.
Apa yang diukur oleh FCP
FCP adalah waktu sejak pengguna mulai membuka halaman hingga browser menampilkan konten pertama: teks, gambar (termasuk background image), SVG, atau <canvas> yang tidak putih. Dari sudut pandang pengguna, FCP menjawab satu pertanyaan: "Apakah ada yang sedang terjadi?"
- Baik: 1,8 detik atau kurang
- Perlu perbaikan: antara 1,8 dan 3,0 detik
- Buruk: lebih dari 3,0 detik
FCP bukan bagian dari tiga metrik Core Web Vitals, tetapi FCP adalah peringatan dini terbaik yang kamu punya. Semua yang memperlambat FCP juga memperlambat LCP, karena tidak ada elemen yang bisa menjadi yang terbesar sebelum ada apa pun yang ditampilkan.
Apa yang diukur oleh LCP
LCP adalah waktu hingga gambar atau blok teks terbesar yang terlihat di viewport selesai di-render. LCP termasuk Core Web Vitals, jadi berpengaruh pada peringkat pencarian, dan jauh lebih mewakili kondisi "halaman sudah bisa dipakai" dibanding metrik lama seperti load atau DOMContentLoaded.
- Baik: 2,5 detik atau kurang
- Perlu perbaikan: antara 2,5 dan 4,0 detik
- Buruk: lebih dari 4,0 detik
Google menilai persentil ke-75 dari pemuatan halaman sungguhan, dipisahkan antara mobile dan desktop. Artinya, tiga dari empat pengunjungmu harus mendapat pengalaman yang baik, bukan hanya yang memakai laptop kencang.
Elemen apa saja yang bisa menjadi elemen LCP
- Elemen
<img>, dan elemen<image>di dalam<svg> - Elemen
<video>(poster image atau frame pertama yang ditampilkan) - Elemen dengan background image yang dimuat lewat
url()di CSS - Elemen block-level yang berisi teks, seperti heading dan paragraf
Browser terus melaporkan kandidat baru saat elemen yang lebih besar muncul, dan berhenti begitu pengguna berinteraksi dengan halaman (tap, scroll, atau menekan tombol). Elemen dengan opacity: 0 diabaikan, begitu juga gambar dengan detail sangat rendah seperti placeholder buram, karena tidak menampilkan konten sungguhan.
Cara mengukur FCP dan LCP
Kamu butuh dua jenis data. Data lab dari Lighthouse atau panel Performance di Chrome DevTools bisa diulang dengan hasil konsisten dan sangat cocok untuk debugging. Data lapangan (field data) dari pengguna sungguhan, seperti Chrome UX Report di PageSpeed Insights atau tool analytics milikmu sendiri, menunjukkan apa yang benar-benar dialami pengunjung. Kalau keduanya berbeda, percayai data lapangan dan gunakan data lab untuk mencari penyebabnya.
Kedua metrik ini disediakan langsung oleh browser, jadi kamu bisa memantaunya dengan PerformanceObserver. Tempel kode berikut di console DevTools, lalu muat ulang halaman:
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log("FCP", Math.round(entry.startTime), "ms");
}
}).observe({ type: "paint", buffered: true });
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lcp = entries[entries.length - 1]; // the latest candidate wins
console.log("LCP", Math.round(lcp.startTime), "ms", lcp.element);
}).observe({ type: "largest-contentful-paint", buffered: true });Observer paint juga melaporkan first-paint, jadi pastikan nama entry-nya first-contentful-paint kalau kamu mencatat lebih dari sekadar waktunya. Untuk monitoring di production, gunakan library web-vitals. Versi attribution-nya tidak hanya memberi tahu seberapa lambat LCP, tetapi juga elemen mana yang menjadi LCP dan ke mana waktunya habis:
import { onFCP, onLCP } from "web-vitals/attribution";
onFCP(({ value, attribution }) => {
console.log("FCP", value, "TTFB", attribution.timeToFirstByte);
});
onLCP(({ value, attribution }) => {
console.log("LCP", value, attribution.target, {
ttfb: attribution.timeToFirstByte,
loadDelay: attribution.resourceLoadDelay,
loadDuration: attribution.resourceLoadDuration,
renderDelay: attribution.elementRenderDelay,
});
});Pecah LCP menjadi empat bagian
"LCP-nya lambat" tidak bisa langsung ditindaklanjuti. "Gambar LCP kita baru mulai diunduh 1,2 detik terlambat" bisa. Setiap waktu LCP dapat dipecah menjadi empat bagian yang berurutan, dan masing-masing punya penyebab serta solusi yang berbeda:
- Time to First Byte (TTFB): dari awal navigasi hingga byte pertama HTML tiba.
- Resource load delay: dari TTFB hingga browser mulai mengunduh resource LCP (biasanya gambar). Bernilai nol untuk teks.
- Resource load duration: berapa lama resource LCP diunduh. Bernilai nol untuk teks.
- Element render delay: dari selesainya unduhan hingga elemen benar-benar ditampilkan.
Pada halaman yang teroptimasi dengan baik, sebagian besar waktu habis untuk TTFB dan unduhan gambar (masing-masing sekitar 40%), sedangkan load delay dan render delay masing-masing di bawah 10%. Kalau salah satu delay itu besar, di situlah perbaikan cepatmu. Panel Performance di Chrome DevTools menampilkan pembagian ini di bagian LCP insight, jadi kamu tidak perlu menghitungnya manual.
Masalah 1: respons server lambat (TTFB tinggi)
Tidak ada yang bisa ditampilkan sebelum HTML tiba, jadi TTFB menjadi batas bawah untuk FCP dan LCP. Penyebab yang umum adalah me-render setiap halaman di setiap request, pemanggilan database atau API yang lambat, rantai redirect yang panjang, dan melayani pengguna dari satu region yang jauh dari mereka.
- Pre-render halaman yang tampil sama untuk semua orang. HTML statis di CDN memberi TTFB tercepat yang bisa kamu dapat. Di Next.js, gunakan
generateStaticParamsuntuk dynamic route, danrevalidatekalau kontennya berubah dari waktu ke waktu. - Simpan data yang mahal di cache, alih-alih mengambilnya di setiap request.
- Hapus redirect dari halaman masuk yang penting, seperti rantai
httpkehttpskewww, dan tautkan langsung ke URL akhirnya. - Gunakan streaming agar browser bisa mulai memproses HTML shell sementara data yang lambat masih dimuat.
import { Suspense } from "react";
export default function Dashboard() {
return (
<>
<PageHeader /> {/* sent immediately, painted early */}
<Suspense fallback={<StatsSkeleton />}>
<SlowStats /> {/* streamed in when the data is ready */}
</Suspense>
</>
);
}Streaming sangat membantu FCP. Namun, streaming hanya membantu LCP kalau elemen LCP termasuk bagian dari shell, jadi letakkan konten hero di luar batas Suspense yang lambat.
Masalah 2: CSS dan JavaScript yang menahan render
Browser tidak akan menampilkan apa pun sebelum selesai mengunduh dan mem-parsing setiap stylesheet di <head>, serta setiap <script> sinkron tanpa async atau defer. Setiap file tersebut adalah satu perjalanan bolak-balik ke server yang berada tepat di jalur menuju FCP.
<!-- Blocks parsing and painting until downloaded and executed -->
<script src="/analytics.js"></script>
<!-- Downloads in parallel and runs after the HTML is parsed -->
<script src="/analytics.js" defer></script>- Tambahkan
defer(atauasyncuntuk script yang berdiri sendiri) ke setiap script yang tidak dibutuhkan untuk tampilan pertama. Di Next.js, muat script pihak ketiga dengannext/scriptdanstrategy="lazyOnload"atau"afterInteractive". - Jaga ukuran CSS tetap kecil. Hapus style yang tidak terpakai, hindari framework CSS besar yang jarang kamu pakai, dan jangan
@importsatu stylesheet dari stylesheet lain, karena setiap import menambah satu request berurutan. - Hindari memuat CSS yang sama dua kali dari bundle atau CDN yang berbeda.
Masalah 3: web font yang menyembunyikan teks
Secara default, banyak browser menyembunyikan teks hingga tiga detik selama font kustom diunduh. Heading-mu mungkin sudah siap ditampilkan, tetapi pengguna hanya melihat ruang kosong, dan FCP maupun LCP ikut menunggu font tersebut. Memuat font lewat @import Google Fonts di CSS membuatnya lebih buruk, karena browser harus mengambil stylesheet-nya dulu baru kemudian menemukan file font-nya.
@font-face {
font-family: "Syne";
src: url("/fonts/syne-800.woff2") format("woff2");
font-weight: 800;
font-display: swap; /* show fallback text immediately, swap when ready */
}Di Next.js, next/font mengerjakan semua ini untukmu: font di-host sendiri, font-display: swap ditambahkan, file-nya di-preload, dan fallback font dengan ukuran yang disesuaikan dibuat otomatis agar pergantian font tidak menggeser tata letak. Di situs ini, mengganti @import Google Fonts dengan next/font menghapus satu request yang menahan render di setiap halaman.
Masalah 4: gambar LCP terlambat ditemukan
Ini penyebab LCP lambat yang paling umum, dan terlihat sebagai resource load delay yang besar. Preload scanner milik browser menemukan gambar di HTML sejak sangat awal, tetapi tidak bisa melihat gambar yang:
- Dipasang sebagai
background-imagedi CSS, yang baru ditemukan setelah stylesheet diunduh dan diterapkan. - Disisipkan lewat JavaScript, misalnya di komponen yang di-render di sisi klien atau library carousel.
- Ditandai dengan
loading="lazy", yang membuat browser menunggu sampai tata letak memastikan gambar berada di dalam viewport.
Lazy loading sangat bagus untuk gambar di bawah layar pertama, tetapi merugikan untuk gambar hero. Jadikan gambar LCP bagian dari HTML awal, muat langsung tanpa lazy loading, dan beri tahu browser bahwa gambar itu penting dengan fetchpriority="high":
<!-- Bad: discovered late and deprioritized -->
<img src="/hero.jpg" loading="lazy" alt="Product preview" />
<!-- Good: discovered by the preload scanner and fetched first -->
<img
src="/hero-1200.avif"
srcset="/hero-800.avif 800w, /hero-1200.avif 1200w, /hero-2000.avif 2000w"
sizes="(min-width: 1024px) 50vw, 100vw"
width="1200"
height="800"
fetchpriority="high"
alt="Product preview"
/>Kalau gambarnya memang harus menjadi background di CSS, preload di <head> agar unduhannya langsung dimulai:
<link rel="preload" as="image" href="/hero-1200.avif" fetchpriority="high" />Dengan next/image, tambahkan prop preload pada gambar LCP (sebelum Next.js 16 namanya priority). Next.js kemudian tidak me-lazy-load gambar itu dan menambahkan preload hint untukmu:
import Image from "next/image";
import hero from "@/assets/images/hero.jpg";
<Image src={hero} alt="Product preview" sizes="(min-width: 1024px) 50vw, 100vw" preload />Gunakan preload dan prioritas tinggi untuk satu atau dua gambar saja. Kalau semuanya ditandai penting, tidak ada yang benar-benar penting.
Masalah 5: gambar LCP terlalu berat
Resource load duration yang besar berarti file-nya sendiri terlalu lama diunduh. Tersangka utamanya adalah gambar yang jauh lebih besar dari ukuran tampilannya, format lama, dan gambar yang disajikan dari server yang lambat atau jauh.
- Sajikan ukuran yang benar-benar dibutuhkan tata letak. Gunakan
srcsetdan atributsizesyang akurat supaya ponsel tidak mengunduh gambar seukuran desktop. - Gunakan format modern. AVIF dan WebP biasanya jauh lebih kecil dari JPEG atau PNG dengan kualitas visual yang sama.
- Kompres dengan benar. Gambar hero jarang butuh kualitas lebih dari sekitar 75 sampai 80.
- Sajikan gambar dari CDN, idealnya dari domainmu sendiri. Gambar di domain lain butuh koneksi tambahan; kalau tidak bisa dihindari, tambahkan
<link rel="preconnect">untuk origin tersebut.
next/image menangani perubahan ukuran, format modern, dan caching secara otomatis. Kamu tetap harus mengisi nilai sizes yang benar, dan bagian inilah yang paling sering terlupa.
Masalah 6: client-side rendering
Single-page app klasik mengirim file HTML yang hampir kosong, kira-kira hanya <div id="root"></div>, lalu membangun halamannya dengan JavaScript. Browser tidak punya apa pun untuk ditampilkan sampai bundle JavaScript selesai diunduh, di-parse, dan dijalankan, serta data yang dibutuhkan selesai diambil. FCP dan LCP sama-sama menunggu seluruh rangkaian itu.
Me-render di server menyelesaikan masalah ini dari akarnya. Dengan Server-Side Rendering atau Static Site Generation, HTML sudah berisi heading, teks, dan tag gambar, sehingga browser bisa menampilkannya selagi JavaScript masih dimuat. Hydration membuat halaman interaktif belakangan, tetapi tidak menahan tampilan pertama. Inilah salah satu alasan terbesar portfolio ini pindah dari aplikasi Vite yang di-render di sisi klien ke Next.js.
Masalah 7: element render delay yang tinggi
Kadang gambar sudah selesai diunduh, atau teksnya sudah ada di HTML, tetapi browser tetap belum menampilkannya. Jeda inilah yang disebut element render delay. Penyebab yang umum:
- Stylesheet atau script yang menahan render dan masih dimuat (lihat masalah 2).
- Task JavaScript yang panjang di main thread sehingga browser sibuk selama pemuatan.
- Konten yang disembunyikan sampai JavaScript berjalan, seperti snippet "anti-flicker" dari tool A/B testing.
- Animasi masuk yang memulai elemen LCP dari
opacity: 0.
Penyebab terakhir mudah terlewat di situs dengan animasi yang cantik, termasuk situs ini. Elemen yang ditampilkan dengan opacity: 0 bukan kandidat LCP, jadi heading hero yang muncul dengan efek fade bisa merusak metrik ini dengan dua cara. Heading itu baru dihitung setelah terlihat, sehingga delay animasi ikut menambah waktu LCP, atau browser melewatinya sama sekali dan melaporkan elemen yang lebih kecil. Saat saya mengukur situs ini di Chrome, LCP justru menunjuk teks logo kecil di navbar, bukan heading hero: angkanya terlihat bagus, tetapi tidak menggambarkan kapan pengunjung benar-benar melihat konten utama. Biarkan elemen LCP terlihat sejak frame pertama dan animasikan pergerakannya saja:
/* Distorts LCP: the heading is invisible until the animation runs */
@keyframes fade-up {
from { opacity: 0; transform: translateY(24px); }
to { opacity: 1; transform: translateY(0); }
}
/* Keeps LCP honest and fast: the heading is painted immediately and only moves */
@keyframes slide-up {
from { transform: translateY(24px); }
to { transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
.hero-title { animation: none; }
}Animasikan konten di sekitar elemen LCP, bukan elemen LCP itu sendiri.
Masalah 8: elemen yang salah menjadi LCP
Selalu cek elemen mana yang menjadi LCP sebelum mengoptimalkan. Sering kali elemennya bukan yang kamu kira. Banner cookie yang besar, pop-up promosi, atau blok teks panjang di mobile bisa lebih besar dari gambar hero. Mengoptimalkan hero tidak ada gunanya kalau ternyata banner cookie yang menjadi elemen LCP dan baru disisipkan belakangan oleh script pihak ketiga.
Gunakan nilai attribution.target dari web-vitals, atau penanda LCP di panel Performance DevTools, untuk melihat elemen yang sebenarnya. Lalu optimalkan elemen itu. Ingat, jawabannya bisa berbeda antara mobile dan desktop, karena ukuran viewport menentukan elemen mana yang terbesar.
Alur kerja langkah demi langkah
- Cek data lapangan di PageSpeed Insights untuk melihat apakah FCP atau LCP memang bermasalah bagi pengguna sungguhan, dan di jenis perangkat apa.
- Temukan elemen LCP di halaman yang lambat untuk jenis perangkat tersebut.
- Pecah waktu LCP menjadi TTFB, load delay, load duration, dan render delay.
- Perbaiki bagian terbesar lebih dulu: TTFB dengan static rendering dan caching, load delay dengan membuat gambar mudah ditemukan dan
fetchpriority, load duration dengan gambar yang lebih kecil, serta render delay dengan mengurangi resource yang menahan render dan tidak menyembunyikan konten. - Rilis perubahannya, lalu pastikan perbaikannya terlihat di data lapangan. Data lapangan diperbarui dalam hitungan hari, bukan menit, jadi terus pantau.
FCP dan LCP bukan skor misterius. Keduanya adalah lini waktu dari apa yang dikerjakan browser, dan setiap milidetiknya milik satu langkah tertentu. Begitu kamu tahu langkah mana yang lambat, perbaikannya biasanya kecil: satu atribut di gambar, defer di sebuah script, atau halaman yang di-render lebih awal alih-alih di setiap request.