Amazon pernah melaporkan bahwa setiap 100ms keterlambatan load berarti 1% kehilangan penjualan. Google menyatakan bahwa 53% pengguna mobile meninggalkan halaman yang butuh lebih dari 3 detik untuk load. Performa bukan hanya soal UX — ini soal revenue.
Artikel ini akan bahas secara mendalam tentang Core Web Vitals, cara mengukurnya, dan teknik optimisasi baik yang populer maupun yang sering terlewat.
Memahami Core Web Vitals
Google memperkenalkan Core Web Vitals sebagai metrik standar untuk mengukur pengalaman pengguna. Sejak 2021, ini menjadi faktor ranking SEO.
LCP — Largest Contentful Paint
LCP mengukur kapan konten terbesar di viewport selesai dirender. "Terbesar" berarti elemen dengan ukuran render area terbesar: bisa image, video poster, atau block teks besar.
Target: ≤ 2.5 detik (Good), 2.5–4s (Needs Improvement), > 4s (Poor)
Yang paling sering jadi LCP element:
- Hero image
- Above-the-fold background image
- H1 atau paragraf pertama kalau tidak ada gambar besar
Cara meningkatkan LCP:
<!-- Prioritaskan load hero image -->
<img
src="/hero.jpg"
fetchpriority="high"
loading="eager"
decoding="sync"
alt="Hero"
/>
<!-- Preload gambar yang akan jadi LCP -->
<link rel="preload" as="image" href="/hero.jpg" />
Untuk background image di CSS yang sering terlewat — browser tidak bisa discover background image dari CSS sampai CSS selesai di-parse dan layout selesai dihitung. Ini menyebabkan LCP lebih lambat:
<!-- Hindari ini untuk hero image -->
<div class="hero-bg"></div>
<!-- Lebih baik pakai img tag dengan object-fit -->
<img
class="hero-image"
src="/hero.jpg"
fetchpriority="high"
alt="Hero"
/>
CLS — Cumulative Layout Shift
CLS mengukur seberapa banyak konten bergeser secara tak terduga selama lifecycle halaman. Ini yang bikin frustrasi saat kamu mau klik tombol, tapi konten geser dan kamu malah klik sesuatu yang lain.
Target: ≤ 0.1 (Good), 0.1–0.25 (Needs Improvement), > 0.25 (Poor)
Penyebab umum CLS:
1. Image tanpa dimensi eksplisit:
<!-- Buruk — browser tidak tahu ukurannya sampai gambar load -->
<img src="/photo.jpg" alt="Photo" />
<!-- Baik — browser reservasi space sebelum gambar load -->
<img src="/photo.jpg" alt="Photo" width="800" height="600" />
<!-- Atau pakai CSS aspect-ratio -->
<img src="/photo.jpg" alt="Photo" style="aspect-ratio: 4/3; width: 100%;" />
2. Ad slots tanpa ukuran fixed:
/* Reservasi space untuk ad sebelum ad load */
.ad-slot {
min-height: 250px;
width: 300px;
}
3. Font yang menyebabkan FOUT/FOIT:
/* Gunakan font-display: swap atau optional */
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-display: swap; /* Tampilkan fallback dulu, swap saat font load */
}
/* Minimize CLS dari font swap dengan size-adjust */
@font-face {
font-family: 'Fallback';
src: local('Arial');
size-adjust: 96%; /* Sesuaikan agar mirip dengan custom font */
ascent-override: 90%;
descent-override: 22%;
}
4. Konten yang di-inject dinamis:
// Buruk — inject banner di atas konten yang sudah ada
document.body.insertBefore(banner, document.body.firstChild);
// Baik — reservasi space di HTML, isi setelah load
// <div id="banner-slot" style="min-height: 60px;"></div>
document.getElementById('banner-slot').appendChild(banner);
INP — Interaction to Next Paint
INP menggantikan FID (First Input Delay) mulai Maret 2024. INP mengukur responsivitas interaksi sepanjang lifecycle halaman — bukan hanya interaksi pertama.
Target: ≤ 200ms (Good), 200–500ms (Needs Improvement), > 500ms (Poor)
INP dihitung dari: input event diterima → browser paint frame berikutnya.
Cara meningkatkan INP:
// Pindahkan heavy computation ke web worker
const worker = new Worker('/workers/heavy-computation.js');
function handleButtonClick() {
// Jangan block main thread
worker.postMessage({ data: largeDataset });
worker.onmessage = ({ data }) => {
updateUI(data.result);
};
}
// Gunakan scheduler API untuk yield ke browser
async function processItems(items) {
for (const item of items) {
processItem(item);
// Yield setiap iterasi agar browser bisa handle input lain
if (navigator.scheduling?.isInputPending()) {
await scheduler.yield();
}
}
}
Teknik Optimisasi JavaScript
Code Splitting dan Lazy Loading
Kirim hanya JavaScript yang dibutuhkan untuk halaman saat ini:
// React — lazy load component yang tidak dibutuhkan di initial render
import { lazy, Suspense } from 'react';
const HeavyChart = lazy(() => import('./HeavyChart'));
const AdminPanel = lazy(() => import('./AdminPanel'));
function App() {
return (
<Suspense fallback={<Spinner />}>
{showChart && <HeavyChart />}
{isAdmin && <AdminPanel />}
</Suspense>
);
}
// Route-based code splitting dengan React Router
const routes = [
{
path: '/',
element: lazy(() => import('./pages/Home')),
},
{
path: '/dashboard',
element: lazy(() => import('./pages/Dashboard')),
},
];
Tree Shaking
Pastikan kamu import hanya yang diperlukan:
// Buruk — import seluruh library lodash (70kb+)
import _ from 'lodash';
const result = _.groupBy(data, 'category');
// Baik — import hanya fungsi yang dipakai
import groupBy from 'lodash/groupBy';
const result = groupBy(data, 'category');
// Lebih baik — gunakan library yang ES module friendly
import { groupBy } from 'lodash-es'; // tree-shakeable
Cek bundle kamu dengan:
npx webpack-bundle-analyzer stats.json
# atau
npx vite-bundle-visualizer
Script Loading Strategies
<!-- Blocking — jangan di <head> untuk non-critical scripts -->
<script src="analytics.js"></script>
<!-- Async — load paralel, eksekusi segera setelah selesai download -->
<!-- Urutan eksekusi tidak dijamin -->
<script async src="analytics.js"></script>
<!-- Defer — load paralel, eksekusi setelah HTML parse selesai -->
<!-- Urutan eksekusi terjaga, sama seperti taruh di akhir <body> -->
<script defer src="app.js"></script>
<!-- Type module — defer by default, support import -->
<script type="module" src="app.js"></script>
Untuk scripts yang tidak kritis (analytics, chatbot, dll):
// Load setelah halaman interactive
window.addEventListener('load', () => {
const script = document.createElement('script');
script.src = 'https://analytics.example.com/tracker.js';
script.async = true;
document.head.appendChild(script);
});
// Atau bahkan lebih lambat — setelah user mulai berinteraksi
let loaded = false;
document.addEventListener('mousemove', () => {
if (!loaded) {
loaded = true;
loadNonCriticalScripts();
}
}, { once: true });
Teknik Optimisasi Image
Image biasanya adalah kontributor terbesar ke page weight. Ini area dengan ROI tertinggi untuk optimisasi.
Format Modern
<!-- WebP dengan PNG fallback -->
<picture>
<source srcset="/hero.webp" type="image/webp" />
<img src="/hero.png" alt="Hero" width="1200" height="630" />
</picture>
<!-- AVIF (kompresi terbaik) dengan fallback berlapis -->
<picture>
<source srcset="/hero.avif" type="image/avif" />
<source srcset="/hero.webp" type="image/webp" />
<img src="/hero.jpg" alt="Hero" width="1200" height="630" />
</picture>
AVIF bisa 50% lebih kecil dari WebP dengan kualitas yang sama. Browser support sudah cukup luas (89%+ per 2024).
Responsive Images
<!-- Kirim ukuran yang tepat berdasarkan viewport -->
<img
src="/photo-800.jpg"
srcset="
/photo-400.jpg 400w,
/photo-800.jpg 800w,
/photo-1200.jpg 1200w,
/photo-1600.jpg 1600w
"
sizes="
(max-width: 640px) 100vw,
(max-width: 1024px) 50vw,
33vw
"
alt="Photo"
width="800"
height="600"
/>
sizes memberitahu browser ukuran display image sebelum CSS di-load, sehingga ia bisa pilih source yang tepat di awal.
Lazy Loading Native
<!-- Gambar di bawah fold — load saat mendekati viewport -->
<img
src="/product.jpg"
loading="lazy"
decoding="async"
alt="Product"
width="400"
height="400"
/>
<!-- Jangan lazy load gambar above the fold! -->
<img
src="/hero.jpg"
loading="eager"
fetchpriority="high"
alt="Hero"
/>
Content Delivery Network untuk Image
Layanan seperti Cloudinary, ImageKit, atau Next.js Image component secara otomatis:
- Serve format yang tepat berdasarkan browser
- Resize berdasarkan viewport
- Compress otomatis
- Cache di edge CDN
// Next.js Image — otomatis handle semua ini
import Image from 'next/image';
<Image
src="/hero.jpg"
width={1200}
height={630}
priority // sama dengan fetchpriority="high" + loading="eager"
alt="Hero"
/>
Resource Hints
Beritahu browser untuk mulai sesuatu sebelum kamu perlu:
preconnect
Buka koneksi TCP + TLS ke origin sebelum resource diminta. Cocok untuk CDN, API, atau font provider:
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preconnect" href="https://api.example.com" />
Simpan untuk maximum 2-3 origin. preconnect yang tidak terpakai adalah waste.
dns-prefetch
Lebih ringan dari preconnect — hanya resolve DNS. Gunakan untuk third-party yang mungkin dibutuhkan tapi tidak yakin:
<link rel="dns-prefetch" href="https://analytics.example.com" />
preload
Load resource penting lebih awal. Gunakan untuk resource yang akan dibutuhkan di halaman saat ini tapi browser tidak discover lebih awal:
<!-- Preload font yang dipakai di CSS -->
<link rel="preload" href="/fonts/custom.woff2" as="font" type="font/woff2" crossorigin />
<!-- Preload critical CSS -->
<link rel="preload" href="/critical.css" as="style" />
<!-- Preload hero image yang ada di CSS background -->
<link rel="preload" href="/hero.jpg" as="image" />
<!-- Preload video yang mungkin diplay segera -->
<link rel="preload" href="/intro.mp4" as="video" type="video/mp4" />
Hati-hati: Terlalu banyak preload justru berbahaya — ia bisa mendeprioritasi resource lain yang lebih penting.
prefetch
Download resource untuk halaman berikutnya yang mungkin dikunjungi:
<!-- User di halaman produk, kemungkinan besar akan ke checkout -->
<link rel="prefetch" href="/checkout.js" />
<link rel="prefetch" href="/checkout.css" />
modulepreload
Khusus untuk ES modules:
<link rel="modulepreload" href="/app.js" />
<link rel="modulepreload" href="/utils.js" />
Critical CSS dan Render Blocking
CSS adalah render-blocking — browser tidak akan render apa pun sampai semua CSS di <head> selesai didownload dan di-parse.
Inline Critical CSS
Ekstrak CSS yang diperlukan untuk render above-the-fold dan inline di <head>:
<head>
<!-- Critical CSS inline — zero additional request -->
<style>
/* Hanya CSS untuk konten above the fold */
body { font-family: system-ui; margin: 0; }
.header { background: #fff; padding: 1rem; }
.hero { height: 500px; background: #f3f4f6; }
.hero h1 { font-size: 2.5rem; font-weight: 700; }
</style>
<!-- Non-critical CSS load async -->
<link rel="preload" href="/styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'" />
<noscript><link rel="stylesheet" href="/styles.css" /></noscript>
</head>
Tools untuk otomatis extract critical CSS:
- Critical (Node.js)
- critters (webpack/Vite plugin)
- Astro dan Next.js melakukan ini otomatis untuk server-rendered pages
Font Performance
Font custom adalah salah satu penyebab terbesar dari CLS dan LCP yang buruk.
Hosting Font Sendiri vs Google Fonts
<!-- Google Fonts — ada 2 request ke domain lain -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
<!-- Self-hosted — lebih cepat, tidak ada third-party dependency -->
<link rel="preload" href="/fonts/inter-400.woff2" as="font" type="font/woff2" crossorigin>
<style>
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-400.woff2') format('woff2');
font-weight: 400;
font-display: optional; /* Tidak ada flash — pakai fallback kalau font tidak ready di render pertama */
}
</style>
font-display options:
-
auto— default browser -
block— FOIT (flash of invisible text) — kosong dulu sampai font load -
swap— FOUT (flash of unstyled text) — fallback dulu, swap saat font ready -
fallback— FOIT singkat (100ms), lalu fallback, lalu swap kalau font sudah load -
optional— pakai font hanya kalau sudah cached atau load sangat cepat — paling baik untuk CLS
Subset Font
Kalau kamu hanya pakai karakter latin, jangan load seluruh font:
/* Hanya load subset Latin */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-latin.woff2') format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
Tools: glyphhanger, pyftsubset.
Teknik yang Sering Terlewat
Brotli Compression
Semua orang tahu gzip, tapi Brotli (dikembangkan Google) bisa 20-26% lebih kecil dari gzip untuk text content.
# Nginx config
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/javascript application/json;
# Apache dengan mod_brotli
AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/css application/javascript
content-visibility: auto
Ini browser hint yang powerful — skip rendering (layout, paint) untuk konten yang off-screen:
.article-card {
content-visibility: auto;
contain-intrinsic-size: 0 300px; /* Estimasi ukuran untuk scrollbar accuracy */
}
Bisa meningkatkan render time 40-60% untuk halaman panjang dengan banyak kartu/item. Browser Chromium support penuh, Firefox support sejak 2023.
Back/Forward Cache (bfcache)
bfcache menyimpan snapshot halaman yang ditinggalkan user — saat mereka klik tombol back/forward, halaman muncul instan karena tidak perlu re-render.
Banyak hal yang bisa membuat halaman tidak bisa disimpan di bfcache:
-
unloadevent listener - Cache-Control:
no-store - Koneksi WebSocket yang tidak diclose
- IndexedDB transactions yang masih terbuka
// Jangan gunakan unload — pakai pagehide sebagai gantinya
// BURUK:
window.addEventListener('unload', cleanup);
// BAIK:
window.addEventListener('pagehide', cleanup);
// Test bfcache eligibility
window.addEventListener('pageshow', (event) => {
if (event.persisted) {
console.log('Halaman di-restore dari bfcache!');
}
});
Priority Hints
Kontrol prioritas resource loading secara eksplisit:
<!-- Turunkan prioritas gambar yang tidak penting -->
<img src="/decoration.png" fetchpriority="low" alt="" />
<!-- Naikkan prioritas resource yang perlu diload duluan -->
<link rel="preload" href="/critical.js" as="script" fetchpriority="high" />
<!-- Turunkan prioritas fetch yang tidak urgent -->
<script>
fetch('/api/recommendations', { priority: 'low' })
.then(r => r.json())
.then(updateRecommendations);
</script>
Tools untuk Pengukuran
Lighthouse — audit performa, aksesibilitas, SEO. Built-in di Chrome DevTools.
PageSpeed Insights — Lighthouse + data dari Chrome User Experience Report (CrUX) — data real user, bukan lab.
WebPageTest — pengukuran mendalam dengan opsi lokasi, device, koneksi yang spesifik.
Chrome DevTools Performance Tab — rekam dan analisis performance trace. Lihat di mana main thread diblokir.
Core Web Vitals Report — di Google Search Console, lihat data CWV dari user sungguhan yang mengunjungi situsmu.
// Ukur CWV secara programmatic dengan web-vitals library
import { onLCP, onINP, onCLS } from 'web-vitals';
onLCP(({ value, rating }) => {
console.log(`LCP: ${value}ms — ${rating}`);
// Kirim ke analytics
sendToAnalytics('LCP', value);
});
onINP(({ value, rating }) => {
console.log(`INP: ${value}ms — ${rating}`);
});
onCLS(({ value, rating }) => {
console.log(`CLS: ${value} — ${rating}`);
});
Prioritas Optimisasi
Kalau harus memilih, urutkan berdasarkan dampak vs effort:
- Compress dan optimalkan image — dampak besar, effort rendah
- Implement browser caching yang benar — dampak besar, effort rendah-medium
- Serve assets dari CDN — dampak besar, effort medium
- Remove render-blocking resources — dampak medium-besar, effort medium
- Lazy load below-the-fold images — dampak medium, effort rendah
- Code split JavaScript — dampak medium-besar, effort medium-tinggi
- Implement critical CSS — dampak medium, effort medium
- Optimalkan font loading — dampak medium, effort medium
Ukur dulu sebelum optimisasi — tidak semua teknik relevan untuk semua website. Data dari PageSpeed Insights dan CrUX adalah titik mulai yang baik.
Top comments (0)