kodingup.camp

Beranda/Kualitas Web: Aksesibilitas, Performa, Keamanan, Testing & Tooling

Performa Web

Modul 3 dari 88 menit bacaWeb Performance

Isi modul

    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:

    Data lab vs data lapangan

    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:

    1. Format modern: WebP dan AVIF biasanya jauh lebih kecil daripada JPEG/PNG untuk kualitas serupa. Pakai <picture> untuk menyediakan cadangan.
    2. Ukuran responsif: srcset berisi beberapa versi lebar gambar, dan sizes memberi tahu browser selebar apa gambar akan tampil. Browser memilih file terkecil yang cukup tajam untuk layar itu. HP tidak perlu mengunduh gambar 2400 px.
    3. width dan height selalu diisi: browser bisa menyiapkan ruang dengan rasio yang benar sebelum gambar tiba, sehingga tidak ada lompatan layout (CLS). Alternatifnya, pakai CSS aspect-ratio.
    4. 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.
    5. 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:

    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:

    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

    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

    1. Lazy-load semua gambar termasuk hero. Gambar LCP jadi terlambat diunduh. Gambar di layar pertama tidak boleh loading="lazy", beri fetchpriority="high".
    2. Gambar tanpa width/height. Konten di bawahnya meloncat saat gambar tiba, CLS naik. Selalu isi keduanya (CSS height: auto menjaga responsif).
    3. Mengukur di laptop kencang lalu merasa aman. Uji dengan throttling CPU dan jaringan, atau lihat data lapangan di PageSpeed Insights.
    4. Preload font tanpa crossorigin. Font diunduh dua kali. Tambahkan crossorigin walau fontnya dari domain sendiri.
    5. Mengejar skor Lighthouse 100 sambil mengabaikan pengguna nyata. Skor lab berguna untuk debugging, tapi yang dinilai adalah persentil ke-75 pengguna sungguhan.

    Latihan

    1. 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?
    2. 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.
    3. 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 ketiganya good).
    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

    Video pendamping

    Kalau penjelasan di atas belum masuk, tonton ini. Video dari kanal YouTube pihak lain.

    Tentang Core Web Vitals | Getting Started | Mau CodingMauCoding · Bahasa IndonesiaBuka di YouTube

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.