Kenapa ini penting
Pernah antre di kasir minimarket yang mesinnya lemot? Barang sudah di meja, uang sudah di tangan, tapi layar kasir masih berputar. Setelah beberapa detik kamu mulai gelisah, dan kalau ada kasir lain yang kosong kamu langsung pindah. Pengguna web berperilaku sama, hanya lebih tidak sabar, karena "kasir lain" cuma berjarak satu tombol Back.
Performa bukan soal skor. Performa adalah soal pengalaman yang dirasakan: seberapa cepat konten utama muncul, seberapa cepat halaman merespons saat diklik, dan apakah tata letaknya diam atau malah meloncat-loncat sehingga kamu salah memencet tombol. Di Indonesia hal ini lebih terasa karena banyak pengguna membuka web dari HP kelas menengah ke bawah dengan kuota terbatas dan sinyal yang naik-turun. Halaman 5 MB yang terasa ringan di laptop kantor dengan WiFi fiber bisa jadi siksaan di HP Rp1,5 juta di dalam angkot.
Performa juga berkaitan dengan aksesibilitas (pengguna dengan perangkat lama ikut terbantu) dan biaya (setiap kilobyte yang tidak perlu dikirim adalah kuota pengguna yang terbuang). Google juga memakai Core Web Vitals sebagai salah satu sinyal pengalaman halaman di penelusuran.
Konsep inti
Core Web Vitals: tiga angka yang mewakili pengalaman
Google, lewat web.dev, merangkum pengalaman pengguna ke dalam tiga metrik yang disebut Core Web Vitals. Ambang di bawah ini diambil langsung dari web.dev dan dinilai pada persentil ke-75 kunjungan, dipisah antara mobile dan desktop. Artinya, 75% kunjunganmu harus berada di zona "baik" agar halaman dinilai baik.
| Metrik | Mengukur | Baik | Perlu perbaikan | Buruk |
|---|---|---|---|---|
| LCP (Largest Contentful Paint) | Kecepatan memuat: kapan elemen konten terbesar (gambar hero, judul besar) selesai tampil | ≤ 2,5 detik | 2,5 - 4 detik | > 4 detik |
| INP (Interaction to Next Paint) | Responsivitas: jeda dari klik/tap/tekan tombol sampai layar menampilkan responsnya | ≤ 200 ms | 200 - 500 ms | > 500 ms |
| CLS (Cumulative Layout Shift) | Stabilitas visual: seberapa banyak elemen bergeser tanpa diminta | ≤ 0,1 | 0,1 - 0,25 | > 0,25 |
Beberapa detail yang sering disalahpahami:
- INP menggantikan FID (First Input Delay) dan menjadi Core Web Vital stabil sejak 2024. FID hanya mengukur jeda interaksi pertama. INP mengamati semua klik, tap, dan penekanan tombol selama kunjungan (hover dan scroll tidak dihitung), lalu melaporkan kira-kira yang terburuk. Untuk setiap 50 interaksi, satu yang paling lambat diabaikan sebagai pencilan.
- Satu interaksi INP terdiri dari tiga fase: input delay (menunggu main thread yang sedang sibuk), processing duration (waktu menjalankan event handler-mu), dan presentation delay (waktu browser menghitung layout dan menggambar frame berikutnya).
- LCP bisa dipecah menjadi empat bagian: Time to First Byte (server merespons), resource load delay (jeda sebelum gambar LCP mulai diunduh), resource load duration (lama unduhnya), dan element render delay (jeda sampai benar-benar tergambar). Panduan kasar dari web.dev: TTFB dan durasi unduh masing-masing sekitar 40%, dua sisanya masing-masing di bawah 10%.
- Pergeseran layout yang terjadi dalam 500 ms setelah input pengguna (misalnya klik "Lihat selengkapnya" lalu konten memanjang) tidak dihitung ke CLS, karena memang diharapkan.
Data lab vs data lapangan
- Data lab diukur di lingkungan terkontrol: Lighthouse di DevTools, atau bagian "Diagnose performance issues" di PageSpeed Insights. Bagus untuk debugging dan bisa diulang. Lighthouse mensimulasikan HP kelas menengah di jaringan mobile. Lighthouse tidak bisa mengukur INP karena tidak ada pengguna yang mengklik, jadi ia memakai TBT (Total Blocking Time) sebagai indikator pengganti.
- Data lapangan (field data) berasal dari pengguna sungguhan. PageSpeed Insights mengambilnya dari Chrome User Experience Report (CrUX) untuk periode 28 hari terakhir. Situs baru atau sepi pengunjung sering belum punya data CrUX.
- Kamu juga bisa mengumpulkan data lapangan sendiri dengan pustaka
web-vitalsdari Google (versi 6.2 saat ditulis).
Kalau keduanya berbeda, percayai data lapangan untuk menilai, dan pakai data lab untuk mencari penyebab.
Gambar: penyumbang byte terbesar
Gambar biasanya bagian terberat halaman dan sering menjadi elemen LCP. Teknik utamanya:
- Format modern: WebP dan AVIF biasanya jauh lebih kecil daripada JPEG/PNG untuk kualitas serupa. Pakai
<picture>untuk menyediakan cadangan. - Ukuran responsif:
srcsetberisi beberapa versi lebar gambar, dansizesmemberi tahu browser selebar apa gambar akan tampil. Browser memilih file terkecil yang cukup tajam untuk layar itu. HP tidak perlu mengunduh gambar 2400 px. widthdanheightselalu diisi: browser bisa menyiapkan ruang dengan rasio yang benar sebelum gambar tiba, sehingga tidak ada lompatan layout (CLS). Alternatifnya, pakai CSSaspect-ratio.loading="lazy"untuk gambar di bawah layar pertama: browser baru mengunduhnya saat pengguna mendekati gambar itu. Jangan pernah lazy-load gambar LCP. web.dev menegaskan hal ini selalu menambah jeda dan memperburuk LCP.fetchpriority="high"pada gambar yang kemungkinan menjadi LCP (cukup satu atau dua gambar, kalau semua diberi prioritas tinggi efeknya hilang).
Font web
Font kustom bisa membuat teks tidak terlihat sampai file font selesai diunduh (flash of invisible text), atau membuat teks berganti font dan bergeser (CLS). Caranya:
- Pakai format WOFF2 dan hanya bobot yang benar-benar dipakai (misalnya 400 dan 700, bukan sembilan bobot).
- Atur
font-display:swapmenampilkan font cadangan dulu lalu menukar (teks cepat terlihat),optionalhanya memakai font kustom kalau sudah tersedia sangat cepat (paling stabil untuk CLS). - Preload font yang dipakai di atas layar pertama dengan
<link rel="preload" as="font" type="font/woff2" crossorigin>. Atributcrossoriginwajib walaupun fontnya dari domain sendiri, karena font selalu diambil dengan mode CORS. Tanpanya, hasil preload tidak terpakai dan font diunduh dua kali. - Pertimbangkan font sistem (
system-ui) yang tidak perlu diunduh sama sekali.
CSS kritis dan JavaScript yang tidak memblokir
Browser tidak menggambar apa pun sebelum CSS di <head> selesai diunduh dan diproses. CSS bersifat render-blocking. Strateginya:
- Critical CSS: gaya minimum untuk tampilan layar pertama ditaruh inline di
<style>dalam<head>, sisanya dimuat belakangan. Untuk situs kecil, cukup pastikan file CSS-mu kecil dan diminifikasi. - JavaScript:
<script type="module">otomatis berperilaku sepertidefer(tidak menghalangi parsing HTML). Untuk script biasa pakaidefer. Pecah kode berat denganimport()dinamis (dibahas di modul Bundler). - INP: jangan menjalankan kerja berat langsung di event handler. Perbarui UI dulu, lalu tunda pekerjaan lain, misalnya dengan
setTimeoutataurequestAnimationFrame, supaya browser sempat menggambar respons.
Cache HTTP
Cache adalah "kulkas" browser: file yang sudah diunduh disimpan dan dipakai lagi tanpa ke server. Header Cache-Control dari server menentukan aturannya:
| Jenis file | Header yang disarankan | Alasan |
|---|---|---|
Aset ber-hash (index-DTMPt3vS.js) |
public, max-age=31536000, immutable |
Nama berubah setiap isinya berubah, jadi aman disimpan setahun |
| HTML | no-cache |
Browser boleh menyimpan, tapi wajib bertanya ke server dulu apakah ada versi baru |
| Data pribadi | no-store |
Jangan disimpan sama sekali |
Vite otomatis memberi hash pada nama file hasil build (lihat modul Bundler), jadi strategi di atas langsung bisa dipakai. Ditambah kompresi (gzip/Brotli) dan CDN, kamu sudah menutup sebagian besar masalah jaringan.
Mengukur dengan DevTools dan PageSpeed
- Tab Lighthouse: jalankan dalam mode Incognito (ekstensi browser bisa mengganggu hasil), pilih Mobile, lalu baca bagian Diagnostics dan Insights.
- Tab Performance: rekam saat halaman dimuat atau saat kamu mengklik. Metrik LCP, INP, dan CLS tampil langsung, dan kamu bisa melihat long task (tugas lebih dari 50 ms) yang memblokir main thread.
- Tab Network: aktifkan throttling (misalnya "Slow 4G") dan centang "Disable cache" untuk merasakan kunjungan pertama dari jaringan lambat.
- PageSpeed Insights (pagespeed.web.dev): masukkan URL publik untuk melihat data lapangan CrUX dan data lab Lighthouse sekaligus.
Contoh kode
HTML berikut menerapkan teknik gambar dan font di atas:
<head>
<!-- Font di atas layar pertama: preload + crossorigin (wajib untuk font) -->
<link rel="preload" href="/fonts/inter-400.woff2" as="font" type="font/woff2" crossorigin />
<style>
/* CSS kritis inline: cukup untuk menggambar layar pertama */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-400.woff2') format('woff2');
font-weight: 400;
font-display: swap; /* teks langsung tampil dengan font cadangan */
}
body { margin: 0; font-family: 'Inter', system-ui, sans-serif; }
.hero img { width: 100%; height: auto; }
</style>
<link rel="stylesheet" href="/assets/style.css" />
</head>
<body>
<section class="hero">
<!-- Gambar LCP: TIDAK lazy, prioritas tinggi, ukuran responsif -->
<picture>
<source type="image/avif"
srcset="/img/hero-480.avif 480w, /img/hero-960.avif 960w, /img/hero-1600.avif 1600w"
sizes="100vw" />
<img src="/img/hero-960.jpg"
srcset="/img/hero-480.jpg 480w, /img/hero-960.jpg 960w, /img/hero-1600.jpg 1600w"
sizes="100vw"
width="1600" height="900"
fetchpriority="high"
alt="Rak buku kayu penuh novel di toko buku kecil" />
</picture>
</section>
<!-- Gambar jauh di bawah: lazy, browser baru mengunduh saat mendekat -->
<img src="/img/peta-toko.webp" width="800" height="500" loading="lazy" decoding="async"
alt="Peta lokasi toko di Jalan Merdeka nomor 12" />
</body>
Untuk mengumpulkan data lapangan dari pengguna sungguhan, pasang pustaka web-vitals (contoh ini sudah kami jalankan di Chromium):
npm install web-vitals
// src/main.js
import { onLCP, onINP, onCLS } from 'web-vitals';
function laporkan({ name, value, rating }) {
console.log(`${name}: ${Math.round(value * 1000) / 1000} (${rating})`);
// Di produksi: kirim ke server analitikmu, misalnya
// navigator.sendBeacon('/api/vitals', JSON.stringify({ name, value, rating }));
}
onLCP(laporkan);
onINP(laporkan, { reportAllChanges: true });
onCLS(laporkan, { reportAllChanges: true });
// → CLS: 0 (good)
// → LCP: 36 (good) (milidetik, di localhost)
// → INP: 64 (good) (setelah tombol Daftar diklik)
Contoh memperbaiki INP: handler yang melakukan kerja berat sebelum memperbarui tampilan.
// ❌ Klik terasa macet: UI baru berubah setelah filter 50.000 produk selesai
tombolFilter.addEventListener('click', () => {
const hasil = semuaProduk.filter(cocokDenganFilter); // berat
tampilkan(hasil);
label.textContent = 'Filter aktif';
});
// ✅ Beri umpan balik dulu, biarkan browser menggambar, baru kerjakan yang berat
tombolFilter.addEventListener('click', () => {
label.textContent = 'Memfilter…';
requestAnimationFrame(() => {
setTimeout(() => {
const hasil = semuaProduk.filter(cocokDenganFilter);
tampilkan(hasil);
label.textContent = 'Filter aktif';
}, 0);
});
});
Untuk mengukur tanpa membuka DevTools, jalankan Lighthouse dari terminal pada hasil build:
npm run build
npm run preview # server preview Vite di http://localhost:4173
# Terminal lain:
npx lighthouse http://localhost:4173 --only-categories=performance --view
# → Hasil uji form newsletter kita (mode mobile simulasi):
# → Performance 100, LCP 0.9 s, CLS 0, TBT 0 ms
Kesalahan umum
- Lazy-load semua gambar termasuk hero. Gambar LCP jadi terlambat diunduh. Gambar di layar pertama tidak boleh
loading="lazy", berifetchpriority="high". - Gambar tanpa
width/height. Konten di bawahnya meloncat saat gambar tiba, CLS naik. Selalu isi keduanya (CSSheight: automenjaga responsif). - Mengukur di laptop kencang lalu merasa aman. Uji dengan throttling CPU dan jaringan, atau lihat data lapangan di PageSpeed Insights.
- Preload font tanpa
crossorigin. Font diunduh dua kali. Tambahkancrossoriginwalau fontnya dari domain sendiri. - Mengejar skor Lighthouse 100 sambil mengabaikan pengguna nyata. Skor lab berguna untuk debugging, tapi yang dinilai adalah persentil ke-75 pengguna sungguhan.
Latihan
- Mudah, Sebuah halaman punya LCP 3,1 detik, INP 150 ms, CLS 0,02. Metrik mana yang masuk kategori "baik", dan mana yang perlu diperbaiki?
- Sedang, Tulis tag
<img>untuk foto produk 1200×800 yang muncul di tengah halaman (bukan layar pertama), tersedia dalam lebar 400, 800, dan 1200 px, dan tampil selebar 50% layar di desktop (min-width 768px) tapi selebar penuh di HP. - Menantang, Tulis fungsi
nilaiVitals({ lcp, inp, cls })yang mengembalikan rating tiap metrik ('good','needs-improvement','poor') memakai ambang web.dev, plus rating keseluruhan (lulus Core Web Vitals hanya jika ketiganyagood).
Lihat solusi
1. INP 150 ms (≤ 200) dan CLS 0,02 (≤ 0,1) sudah baik. LCP 3,1 detik masuk "perlu perbaikan" (di antara 2,5 dan 4 detik). Mulai cek gambar hero: apakah di-lazy-load, terlalu besar, atau ditemukan terlambat.
2.
<img src="/img/produk-800.webp"
srcset="/img/produk-400.webp 400w, /img/produk-800.webp 800w, /img/produk-1200.webp 1200w"
sizes="(min-width: 768px) 50vw, 100vw"
width="1200" height="800"
loading="lazy" decoding="async"
alt="Tas ransel kanvas warna hijau army dengan dua kantong depan" />
3.
const AMBANG = { lcp: [2500, 4000], inp: [200, 500], cls: [0.1, 0.25] };
function rating(nilai, [baik, buruk]) {
if (nilai <= baik) return 'good';
if (nilai <= buruk) return 'needs-improvement';
return 'poor';
}
function nilaiVitals(metrik) {
const hasil = {};
for (const [nama, nilai] of Object.entries(metrik)) {
hasil[nama] = rating(nilai, AMBANG[nama]);
}
hasil.lulus = Object.values(hasil).every((r) => r === 'good');
return hasil;
}
console.log(nilaiVitals({ lcp: 3100, inp: 150, cls: 0.02 }));
// → { lcp: 'needs-improvement', inp: 'good', cls: 'good', lulus: false }
Ringkasan
- Core Web Vitals: LCP ≤ 2,5 s, INP ≤ 200 ms, CLS ≤ 0,1, dinilai di persentil ke-75 pengguna.
- INP menggantikan FID sejak 2024 dan mengukur respons semua interaksi, bukan hanya yang pertama.
- Gambar: format modern,
srcset/sizes, selaluwidth/height,lazyhanya untuk yang di bawah layar,fetchpriority="high"untuk gambar LCP. - Font: WOFF2,
font-display, preload dengancrossorigin. CSS kritis inline, JavaScriptdefer/module. - Aset ber-hash di-cache setahun (
immutable), HTMLno-cache. - Ukur dengan Lighthouse/Performance (lab) dan PageSpeed Insights/CrUX/
web-vitals(lapangan).