DEV Community

Cover image for Web Performance: Dari Core Web Vitals Sampai Optimisasi yang Sering Dilupakan
isntboxs
isntboxs

Posted on

Web Performance: Dari Core Web Vitals Sampai Optimisasi yang Sering Dilupakan

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" />
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

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%;" />
Enter fullscreen mode Exit fullscreen mode

2. Ad slots tanpa ukuran fixed:

/* Reservasi space untuk ad sebelum ad load */
.ad-slot {
  min-height: 250px;
  width: 300px;
}
Enter fullscreen mode Exit fullscreen mode

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%;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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')),
  },
];
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Cek bundle kamu dengan:

npx webpack-bundle-analyzer stats.json
# atau
npx vite-bundle-visualizer
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

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" />
Enter fullscreen mode Exit fullscreen mode

modulepreload

Khusus untuk ES modules:

<link rel="modulepreload" href="/app.js" />
<link rel="modulepreload" href="/utils.js" />
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode
# Apache dengan mod_brotli
AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/css application/javascript
Enter fullscreen mode Exit fullscreen mode

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 */
}
Enter fullscreen mode Exit fullscreen mode

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:

  • unload event 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!');
  }
});
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

Prioritas Optimisasi

Kalau harus memilih, urutkan berdasarkan dampak vs effort:

  1. Compress dan optimalkan image — dampak besar, effort rendah
  2. Implement browser caching yang benar — dampak besar, effort rendah-medium
  3. Serve assets dari CDN — dampak besar, effort medium
  4. Remove render-blocking resources — dampak medium-besar, effort medium
  5. Lazy load below-the-fold images — dampak medium, effort rendah
  6. Code split JavaScript — dampak medium-besar, effort medium-tinggi
  7. Implement critical CSS — dampak medium, effort medium
  8. 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)