Web Artisan
Chapter 05 · Web Vitals

Cumulative Layout Shift

CLS mengukur seberapa sering pengguna melihat konten di layar mereka tiba-tiba bergerak tanpa diduga. Ini bukan soal kecepatan — halaman bisa load cepat tapi tetap terasa tidak nyaman jika tombol loncat sebelum diklik, atau artikel yang sedang dibaca tiba-tiba bergeser ke bawah karena iklan muncul.

Visual Stability~32 menit baca

Di antara tiga Core Web Vitals — LCP, INP, dan CLS — CLS adalah satu-satunya yang bukan soal waktu. LCP mengukur seberapa cepat konten terbesar muncul. INP mengukur seberapa cepat browser merespons interaksi. Namun CLS mengukur seberapa stabil tata letak halaman selama user membacanya.

Ketidakstabilan visual adalah sumber frustrasi yang sering diremehkan tim engineering. User yang hendak mengklik tombol “Beli” mendadak mengklik tombol “Hapus Keranjang” karena sebuah iklan muncul dan menggeser semuanya. Artikel yang sedang dibaca tiba-tiba loncat dua paragraf ke bawah karena gambar tanpa dimensi baru selesai diunduh. Ini bukan sekadar buruk secara estetika — ini memengaruhi konversi dan kepercayaan pengguna secara nyata.

Chapter ini membedah CLS dari akar: bagaimana browser menghitungnya, penyebab paling umum di lapangan, dan cara memperbaiki masing-masing. Kita mulai dari formula matematis agar kamu tidak sekadar “merasa halaman stabil”, lalu bergerak ke penyebab satu per satu — gambar, font, iklan, konten dinamis, animasi — dan diakhiri dengan teknik debug yang bisa langsung dipakai di DevTools.

01

Apa itu CLS dan Bagaimana Dihitung?

Formula, threshold, dan konsep unexpected shift

CLS bukan sekadar “apakah ada yang bergerak” — ia menghitung akumulasi semua pergeseran tak terduga selama seluruh masa hidup halaman, dengan bobot proporsional terhadap seberapa besar dan seberapa jauh elemen bergerak.

Ketika browser merender ulang tata letak (layout) dan sebuah elemen bergerak dari posisi sebelumnya, terjadi sebuah layout shift. Tidak semua layout shift itu buruk — jika user mengklik tombol “Tampilkan komentar” dan area komentar muncul mendorong konten ke bawah, itu adalah shift yang diharapkan (expected shift). Yang menjadi masalah adalah unexpected shift: pergeseran yang terjadi tanpa dipicu langsung oleh tindakan user.

Secara teknis, sebuah shift dianggap user-initiated — dan tidak dihitung ke CLS — jika terjadi dalam jendela 500 milidetik setelah interaksi user seperti klik, ketuk layar, atau penekanan tombol keyboard. Di luar jendela itu, semua shift dianggap tak terduga dan masuk ke perhitungan CLS.

Rumus dasar layout shift score per shift:

plaintext
layout_shift_score = impact_fraction × distance_fraction

Impact fraction adalah persentase viewport yang terdampak oleh elemen yang bergerak — dihitung dari gabungan area yang ditempati elemen sebelum dan sesudah bergerak. Jika sebuah gambar berukuran 50% tinggi viewport bergerak, dan posisi awal dan akhirnya bersama-sama menutupi 75% viewport, maka impact fraction = 0.75.

Distance fraction adalah jarak terjauh yang ditempuh elemen yang bergerak, dibagi dengan dimensi viewport terbesar (lebar atau tinggi). Jika sebuah elemen bergerak 120px ke bawah pada viewport setinggi 800px, maka distance fraction = 120 / 800 = 0.15.

Untuk kasus di atas: layout_shift_score = 0.75 × 0.15 = 0.1125.

flowchart TD
  VP["Viewport 800px tinggi"]
  E1["Elemen sebelum shift\n posisi: 0-400px = 50%"]
  E2["Elemen setelah shift\n posisi: 120-520px = 50%"]
  UNION["Gabungan area = 0-520px = 65%\n impact_fraction = 0.65"]
  DIST["Jarak tempuh = 120px\n distance_fraction = 120/800 = 0.15"]
  SCORE["layout_shift_score = 0.65 x 0.15 = 0.0975"]

  VP --> E1
  VP --> E2
  E1 --> UNION
  E2 --> UNION
  E1 --> DIST
  E2 --> DIST
  UNION --> SCORE
  DIST --> SCORE

Diagram 1. Cara menghitung impact fraction dan distance fraction dari satu layout shift event.

Nilai CLS akhir bukan sekadar jumlah semua shift score. Browser menggunakan mekanisme session window: shift-shift yang terjadi berdekatan waktu (gap maksimal 1 detik antara shift berurutan, dan total window maksimal 5 detik) dikelompokkan menjadi satu session window. CLS diambil dari nilai session window terbesar. Ini mencegah halaman yang memiliki satu periode buruk dihukum jauh lebih berat dari halaman yang memiliki banyak periode buruk kecil yang tersebar.

KategoriNilai CLSStatus
Good≤ 0.1Halaman stabil secara visual
Needs Improvement0.1 – 0.25Ada shift yang mengganggu
Poor> 0.25Halaman sangat tidak stabil
🧩Analogi: Buku yang Digoyang Saat Dibaca

Bayangkan kamu sedang membaca buku fisik. Setiap kali halaman buku digoyang — entah ada teman yang menepuk meja atau seseorang menarik kertas dari bawah buku — kamu harus mencari ulang baris yang kamu baca. CLS seperti mengukur total “goyang” yang dialami pembaca selama membaca satu halaman. Bukan hanya seberapa keras sekali goyang, tapi akumulasi semua goyang kecil dan besar selama sesi membaca.

Penting untuk memahami bahwa CLS diukur di lapangan, bukan hanya di lab. Pengguna yang membuka halaman dengan koneksi 4G lambat akan mengalami CLS berbeda dari pengguna broadband, karena resource (gambar, font, iklan) datang dengan urutan dan waktu berbeda. Inilah mengapa field data dari Chrome User Experience Report (CrUX) jauh lebih relevan dari sekadar skor Lighthouse lokal.

📝Yang baru kamu pelajari

CLS = nilai session window terbesar dari akumulasi (impact_fraction × distance_fraction) per shift. Hanya unexpected shift yang dihitung — shift dalam 500ms setelah interaksi user diabaikan.

Di section berikutnya kita mulai membedah penyebab pertama dan paling klasik: gambar yang tidak punya dimensi eksplisit.

02

Gambar Tanpa Dimensi dan Aspek Rasio

Penyebab CLS paling klasik dan cara memperbaikinya

Setiap elemen <img> yang tidak memiliki atribut width dan height adalah bom waktu CLS — browser tidak tahu berapa ruang yang harus disisihkan sebelum gambar selesai diunduh.

Ketika browser mem-parse HTML dan menemukan tag <img>, ia perlu memutuskan: seberapa besar ruang yang harus disisihkan untuk gambar ini di tata letak? Jika tidak ada atribut width dan height, browser tidak bisa menjawab pertanyaan ini. Ia akan membuat gambar memiliki ukuran nol, render konten di bawahnya seolah-olah gambar tidak ada, lalu — ketika gambar selesai diunduh dan ukuran aslinya diketahui — tiba-tiba menyisipkan tinggi gambar ke dalam alir dokumen. Semua konten di bawah gambar tergeser ke bawah. Inilah layout shift.

Fix-nya terdengar sederhana tapi sering dilewati: selalu tulis atribut width dan height pada setiap <img>.

HTML
<!-- Sebelum: tidak ada dimensi → layout shift saat gambar datang --> <img src="/produk/serum-vitamin-c.jpg" alt="Serum Vitamin C 30ml"> <!-- Sesudah: atribut width dan height diberikan → browser reserve space --> <img src="/produk/serum-vitamin-c.jpg" alt="Serum Vitamin C 30ml" width="600" height="400" >

Apakah ini berarti gambar akan selalu ditampilkan 600×400 piksel? Tidak. Browser modern menggunakan nilai width dan height sebagai petunjuk aspek rasio, bukan nilai final. Jika CSS kamu mengubah gambar menjadi width: 100%; height: auto;, browser tetap akan menghormati rasio 600:400 untuk menyisihkan ruang yang proporsional — ini disebut intrinsic size hint.

CSS
/* CSS ini tidak membatalkan manfaat atribut width/height */ img { width: 100%; height: auto; }

Cara modern yang lebih eksplisit adalah menggunakan properti CSS aspect-ratio langsung pada container atau elemen gambar:

CSS
.product-image-wrapper { aspect-ratio: 3 / 2; overflow: hidden; } .product-image-wrapper img { width: 100%; height: 100%; object-fit: cover; }

Pendekatan ini sangat berguna ketika ukuran gambar tidak diketahui di HTML (misalnya URL gambar datang dari API dan dimensinya tidak di-embed di URL). Dengan aspect-ratio pada wrapper, browser tetap dapat menyisihkan ruang yang tepat.

Video dan iframe memiliki masalah yang sama. YouTube embed yang populer digunakan di halaman artikel atau halaman kursus sering menjadi sumber CLS tersembunyi karena tidak memiliki tinggi yang ditetapkan:

HTML
<!-- YouTube embed tanpa tinggi tetap → layout shift saat iframe load --> <iframe src="https://www.youtube.com/embed/abc123"></iframe> <!-- Perbaikan: gunakan wrapper dengan aspect-ratio --> <div style="position: relative; aspect-ratio: 16 / 9;"> <iframe src="https://www.youtube.com/embed/abc123" style="position: absolute; inset: 0; width: 100%; height: 100%;" frameborder="0" allowfullscreen ></iframe> </div>

Teknik “padding-top hack” yang menggunakan padding-top: 56.25% pada wrapper adalah versi lama dari pendekatan ini — sebelum aspect-ratio didukung secara luas. Hari ini aspect-ratio jauh lebih bersih dan readable.

Tipe ElemenPenyebab CLSFix
<img>Tidak ada width / heightTambah atribut, pakai height: auto di CSS
<video>Poster image tanpa dimensiSet width dan height pada elemen <video>
<iframe>Tidak ada tinggi tetapWrapper dengan aspect-ratio CSS
CSS background-imageContainer tanpa dimensi tetapSet aspect-ratio atau tinggi eksplisit pada container
💡Pro Tip: content-visibility dan CLS

Properti content-visibility: auto memungkinkan browser melewati rendering konten yang berada di luar viewport untuk mempercepat initial render. Namun jika kamu tidak menyertakan contain-intrinsic-size pada elemen tersebut, browser tidak tahu berapa ruang yang harus disisihkan — dan saat user men-scroll ke bagian itu, terjadi layout shift. Selalu pasangkan keduanya: content-visibility: auto; contain-intrinsic-size: 0 500px; (estimasikan tinggi elemen).

📝Yang baru kamu pelajari

Atribut width dan height pada <img> adalah instruksi aspek rasio untuk browser, bukan ukuran render final. Browser menggunakannya untuk menyisihkan ruang sebelum gambar selesai diunduh — ini mencegah layout shift tanpa mengorbankan fleksibilitas CSS.

Setelah gambar, penyebab CLS berikutnya yang sering mengejutkan adalah font — khususnya bagaimana font web loading berinteraksi dengan tata letak teks.

03

Font Loading: FOIT, FOUT, dan font-display

Mengapa font web bisa memicu layout shift dan cara mencegahnya

Font web yang datang terlambat tidak hanya membuat teks “berkedip” — ia bisa mengubah ukuran teks cukup signifikan untuk menggeser seluruh layout di bawahnya, menghasilkan CLS yang susah dideteksi karena terjadi cepat.

Browser menangani situasi “font belum siap” dengan dua cara berbeda yang masing-masing memiliki trade-off:

FOIT (Flash of Invisible Text): Browser menyembunyikan teks sama sekali sampai font custom siap. Dari sisi CLS, ini aman — tidak ada teks yang ter-render dengan ukuran berbeda, jadi tidak ada layout shift. Namun dari sisi pengguna, ini buruk: teks menjadi tak terlihat selama beberapa ratus milidetik (atau bahkan 3 detik pada koneksi lambat), membuat halaman tampak rusak.

FOUT (Flash of Unstyled Text): Browser langsung menampilkan teks dengan font fallback (biasanya Arial, Georgia, atau font sistem), lalu mengganti dengan font custom saat siap. Pengguna selalu melihat teks — ini lebih baik untuk experience. Namun ada risiko CLS: font custom seringkali memiliki ukuran, spasi, dan kerning berbeda dari font fallback, sehingga saat penggantian terjadi, tinggi dan lebar blok teks berubah, menggeser konten di bawahnya.

Properti CSS font-display menentukan perilaku mana yang digunakan:

CSS
@font-face { font-family: 'Inter'; src: url('/fonts/Inter-Regular.woff2') format('woff2'); font-display: swap; /* FOUT — ada CLS risk, tapi teks selalu terlihat */ }
Nilai font-displayPerilakuCLS RiskTeks Terlihat?
autoTergantung browser (biasanya block)RendahTidak (sementara)
blockFOIT: teks disembunyikan hingga 3 detikSangat RendahTidak (sementara)
swapFOUT: fallback lalu ganti ke customTinggiYa
fallbackBlock singkat (100ms), lalu swap jika sudah siapSedangYa (setelah 100ms)
optionalPakai custom hanya jika sudah cache; jika belum, pakai fallback seterusnyaSangat RendahYa

Untuk mengurangi besarnya FOUT-shift saat menggunakan font-display: swap, ada beberapa pendekatan modern:

1. Preload font critical: Minta browser mengunduh font lebih awal, sebelum browser menemukan referensi font di CSS:

HTML
<!-- Di <head>, sebelum stylesheet --> <link rel="preload" href="/fonts/Inter-Regular.woff2" as="font" type="font/woff2" crossorigin >

Atribut crossorigin wajib ada meskipun font berada di domain yang sama — browser mensyaratkan ini untuk font preload agar mode CORS cocok dengan request font aslinya.

2. size-adjust descriptor: CSS modern memungkinkan kamu mengatur skala fallback font agar mendekati ukuran font custom:

CSS
@font-face { font-family: 'Inter-fallback'; src: local('Arial'); size-adjust: 107%; /* sesuaikan sampai ukuran mendekati Inter */ ascent-override: 90%; /* kontrol ascent metric */ descent-override: 22%; /* kontrol descent metric */ line-gap-override: 0%; } body { font-family: 'Inter', 'Inter-fallback', sans-serif; }

Dengan size-adjust, fallback font Arial akan di-scale sehingga ukurannya mendekati Inter. Ketika Inter akhirnya dimuat, pergeseran layout menjadi minimal karena kedua font mengisi ruang yang hampir sama.

3. Font subsetting: Mengirim hanya karakter yang benar-benar dibutuhkan:

CSS
@font-face { font-family: 'Inter'; src: url('/fonts/Inter-latin.woff2') format('woff2'); unicode-range: U+0000-00FF; /* hanya karakter Latin dasar */ }

Font WOFF2 yang lebih kecil berarti waktu unduh lebih singkat, sehingga jendela FOUT menjadi lebih pendek, dan kemungkinan pengguna mengalami pergeseran berkurang.

flowchart LR
  A["Browser parse HTML"] --> B["Temukan referensi font di CSS"]
  B --> C{Font sudah di cache?}
  C -- Ya --> D["Render langsung dengan font custom\n tidak ada FOUT/FOIT"]
  C -- Tidak --> E["font-display: swap"]
  E --> F["Render dengan fallback font\n FOUT terjadi"]
  F --> G["Font custom selesai download"]
  G --> H["Ganti ke font custom\n CLS terjadi jika ukuran berbeda"]
  H --> I["size-adjust memperkecil delta\n CLS berkurang"]

Diagram 2. Alur font loading dengan font-display: swap dan peran size-adjust dalam meminimalkan CLS.

🌉Jembatan: Docker & Font Preload

Docker menyelesaikan “works on my machine” dengan memaket semua runtime dan dependency bersama aplikasi — tidak ada lagi kejutan “library versi berbeda di production”. Preloading font melakukan hal serupa: alih-alih menunggu browser menemukan font saat mem-parse CSS, kita mengantarkan font lebih awal bersama HTML. Hasilnya, tidak ada lagi kejutan “font beda saat load pertama”.

⚠️Jangan preload semua font

Preload adalah sinyal prioritas tinggi — jika kamu me-preload terlalu banyak font, browser akan bersaing mengunduh semua itu di awal, berpotensi menunda resource yang lebih penting seperti CSS critical-path atau gambar LCP. Preload hanya font yang benar-benar dipakai above the fold, biasanya satu atau dua file font maksimum.

📝Yang baru kamu pelajari

FOUT menyebabkan CLS ketika font custom memiliki ukuran berbeda dari fallback. Trio solusinya: font-display: optional atau fallback untuk kurangi risk, preload untuk persingkat jendela FOUT, dan size-adjust untuk perkecil delta layout saat swap terjadi.

Kita beralih ke kategori penyebab CLS yang sering menghasilkan skor paling tinggi di lapangan: konten yang diinjeksi secara dinamis setelah halaman load.

04

Iklan, Embed, dan Konten Dinamis

Cara konten yang datang terlambat merusak stabilitas visual

Konten yang diinjeksi setelah halaman selesai render — iklan, cookie banner, chat widget, atau personalisasi via JavaScript — adalah penyumbang terbesar CLS di situs nyata, karena semuanya menggeser konten yang sudah ada tanpa user mengklik apapun.

Iklan dinamis (Ad Slots)

Ad slot adalah penyebab CLS tertinggi yang paling sering dilaporkan. Mekanismenya sederhana: container iklan berukuran nol atau sangat kecil saat halaman dimuat, script iklan dijalankan secara asinkron, kemudian iklan diinjeksi dengan ukuran penuh (misalnya 728×90 leaderboard atau 300×250 rectangle) — menggeser semua konten di bawahnya.

Solusi terbaik bukan menghilangkan iklan, tetapi mereservasi ruang di awal:

CSS
.ad-slot { min-height: 90px; /* reservasi untuk leaderboard banner */ width: 100%; background: #f5f5f5; /* placeholder visual agar tidak terlihat kosong */ } /* Untuk unit yang lebih besar */ .ad-slot--rectangle { min-height: 250px; min-width: 300px; }

Bahkan ketika iklan tidak ditampilkan (user menggunakan adblocker, atau slot tidak terisi), ruang tetap ada — tidak ada pergeseran karena tidak ada yang berubah.

Cookie Banner dan Consent Dialog

Cookie banner yang muncul setelah halaman dimuat dan mendorong konten ke bawah adalah sumber CLS klasik lainnya. Ada dua pendekatan fix:

HTML
<!-- Pendekatan 1: Render dari sisi server, bukan inject via JS --> <!-- Server langsung menyertakan banner di HTML jika cookie consent belum ada --> <div id="cookie-banner" class="banner-fixed"> Kami menggunakan cookie... <button>Terima</button> </div>
CSS
/* Pendekatan 2: Gunakan posisi fixed atau sticky — tidak menggeser dokumen */ .banner-fixed { position: fixed; bottom: 0; left: 0; right: 0; z-index: 1000; /* Banner di atas konten, bukan mendorong konten */ }

Pendekatan position: fixed atau position: sticky tidak berkontribusi ke CLS karena elemen tersebut dikeluarkan dari flow normal dokumen — tidak ada konten yang terdorong.

Skeleton Loaders yang Salah

Skeleton loader adalah teknik baik yang bisa menjadi bumerang jika ukurannya tidak akurat:

HTML
<!-- Skeleton 200px tinggi, konten nyata 380px tinggi → shift 180px --> <div class="skeleton" style="height: 200px;"></div> <!-- Saat konten nyata datang, shift CLS muncul! -->

Fix-nya adalah mengukur konten nyata dan mereservasi ruang yang akurat, atau menggunakan min-height yang konservatif (sedikit lebih besar dari estimasi):

CSS
.product-card-skeleton { min-height: 400px; /* sedikit lebih besar dari rata-rata product card */ border-radius: 8px; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; }

Animasi yang Memicu Layout vs Animasi yang Aman

Ini adalah perbedaan kritis yang sering diabaikan: tidak semua properti CSS diciptakan sama dalam konteks CLS dan performa rendering.

Properti yang memicu layout recalculation akan selalu memengaruhi CLS jika dianimasikan tanpa user interaction:

CSS
/* BERBAHAYA: properti ini trigger layout recalc → bisa CLS */ .element { transition: margin-top 0.3s ease; /* hindari */ transition: height 0.3s ease; /* hindari */ transition: top 0.3s ease; /* hindari */ transition: width 0.3s ease; /* hindari */ }

Sebaliknya, transform dan opacity dijalankan di compositor thread — terpisah dari main thread dan tidak memicu layout recalculation:

CSS
/* AMAN: transform dan opacity tidak trigger layout */ .element { transition: transform 0.3s ease; /* AMAN */ transition: opacity 0.3s ease; /* AMAN */ } /* Contoh: slide-in panel dari atas */ .notification-banner { transform: translateY(-100%); opacity: 0; transition: transform 0.3s ease, opacity 0.3s ease; } .notification-banner.visible { transform: translateY(0); opacity: 1; }
Properti CSSTrigger Layout?Trigger Paint?Trigger Composite?CLS Risk
transformTidakTidakYaTidak ada
opacityTidakTidakYaTidak ada
top / leftYaYaYaTinggi
margin / paddingYaYaYaTinggi
width / heightYaYaYaTinggi
background-colorTidakYaYaTidak ada
flowchart LR
  A["Perlu animasi elemen?"] --> B{Gunakan properti apa?}
  B --> C["transform / opacity\n Compositor thread\n Tidak trigger layout"]
  B --> D["top / left / margin / width\n Main thread\n Trigger layout recalc"]
  C --> E["CLS = 0\n Performa tinggi"]
  D --> F["CLS mungkin terjadi\n Performa buruk"]
  F --> G["Solusi: ganti ke transform\n translateX/translateY\n scale / rotate"]
  G --> E

Diagram 3. Alur keputusan memilih properti CSS untuk animasi agar tidak memicu layout shift.

⚠️Chat Widget dan Third-Party Script

Chat widget populer (Intercom, Crisp, Zendesk) sering menjadi sumber CLS tersembunyi. Widget tersebut menginjeksi elemen ke dalam DOM setelah halaman selesai load dan menggunakan position: fixed — yang seharusnya aman. Namun beberapa implementasi mengubah padding-bottom pada body untuk mencegah widget menutupi konten, yang justru memicu layout shift besar. Periksa selalu third-party script menggunakan DevTools sebelum menganggap CLS bersumber dari kode sendiri.

📝Yang baru kamu pelajari

Tiga trik utama menghadapi konten dinamis: reservasi ruang di awal (min-height pada container), gunakan position: fixed/sticky untuk elemen overlay, dan animasikan hanya dengan transform dan opacity — bukan properti yang memicu layout recalculation.

Setelah memahami semua penyebab, kini saatnya belajar menemukan dan mengidentifikasi CLS menggunakan DevTools.

05

Debug CLS di DevTools

Cara menemukan elemen yang menyebabkan layout shift

Mengetahui bahwa CLS buruk di laporan PageSpeed Insights hanyalah titik awal — langkah selanjutnya adalah mengidentifikasi elemen spesifik mana yang bergerak, kapan bergeraknya, dan mengapa, sehingga kamu bisa menargetkan perbaikan dengan tepat.

1. PerformanceObserver via JavaScript

Layout Shift API dapat diakses langsung dari JavaScript menggunakan PerformanceObserver. Kode berikut mencatat setiap shift yang terjadi ke konsol beserta detail attribution-nya:

javascript
// Salin ke Console DevTools untuk monitoring real-time let clsValue = 0; let clsEntries = []; const observer = new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { // Hanya hitung unexpected shift (bukan yang dipicu user interaction) if (!entry.hadRecentInput) { clsValue += entry.value; clsEntries.push(entry); console.group(`Layout Shift — score: ${entry.value.toFixed(4)}`); console.log('CLS kumulatif sejauh ini:', clsValue.toFixed(4)); console.log('Waktu:', entry.startTime.toFixed(0), 'ms'); // Attribution: elemen mana yang bergerak? for (const source of entry.sources || []) { console.log('Elemen:', source.node); console.log('Rect sebelum:', source.previousRect); console.log('Rect sesudah:', source.currentRect); } console.groupEnd(); } } }); observer.observe({ type: 'layout-shift', buffered: true });

Properti hadRecentInput adalah penanda apakah shift ini terjadi dalam 500ms setelah interaksi user — jika true, shift ini tidak dihitung ke CLS dan kita pun melewatinya.

2. web-vitals library dengan Attribution

Untuk integrasi yang lebih terstruktur, gunakan library web-vitals dari Google dengan mode attribution:

javascript
import { onCLS } from 'web-vitals/attribution'; onCLS((metric) => { console.log('CLS value:', metric.value); // Attribution: apa sumber shift terbesar? const { largestShiftSource, largestShiftTarget } = metric.attribution; if (largestShiftSource) { console.log('Shift terbesar dari:', largestShiftSource.node); console.log('Initial rect:', largestShiftSource.initialRect); console.log('Final rect:', largestShiftSource.finalRect); } // Kirim ke analytics sendToAnalytics({ metric: 'CLS', value: metric.value, rating: metric.rating, // 'good', 'needs-improvement', 'poor' element: largestShiftSource?.node?.tagName, }); });

3. Chrome DevTools — Performance Panel

Cara visual untuk menemukan layout shift di DevTools:

Tekan F12 atau Cmd+Option+I (Mac). Pindah ke tab Performance.
Klik ikon ⚙️ (Settings) di panel Performance, aktifkan Layout Shift Regions — ini menampilkan overlay berwarna biru pada elemen yang mengalami shift saat recording.
Klik tombol Record (lingkaran merah), reload halaman, biarkan halaman selesai loading, lalu klik Stop.
Di timeline, cari marker merah berlabel LS (Layout Shift). Klik untuk melihat detail — panel bawah menampilkan node apa yang bergerak.
Di bagian atas timeline, aktifkan Screenshots untuk melihat filmstrip. Perhatikan frame tepat sebelum dan sesudah marker LS — ini memperlihatkan secara visual apa yang bergerak.

4. WebPageTest — Filmstrip Mode

Untuk analisis dari perspektif pengguna nyata dengan berbagai kondisi jaringan:

Terminal
# Jalankan WebPageTest via CLI (jika menggunakan webpagetest-api) wpt test https://example.com \ --key YOUR_API_KEY \ --location "Dulles:Chrome" \ --connectivity 4G \ --filmstrip

WebPageTest menghasilkan filmstrip frame-by-frame yang memperlihatkan kapan tepatnya shift terjadi — berguna untuk menemukan shift yang hanya muncul pada kondisi jaringan tertentu.

5. Workflow Debug CLS End-to-End

Buka PageSpeed Insights untuk URL target. Lihat nilai CLS dari CrUX data (bukan lab) dan perhatikan distribusi P75 — inilah CLS yang dirasakan 75% pengguna.
Gunakan Search Console → Core Web Vitals report untuk menemukan URL mana yang paling banyak dilaporkan Poor.
Di DevTools, set CPU throttling 4x dan network throttling ke “Slow 4G”. Reload halaman dan amati apakah ada pergeseran visual yang terlihat.
Salin snippet dari langkah 1 ke DevTools Console. Reload halaman dan perhatikan log — entry mana yang memiliki score tertinggi?
Dari log observer, klik referensi node untuk jump ke panel Elements. Periksa CSS yang diterapkan pada elemen dan container induknya.
Terapkan fix (tambah dimensi, ubah animasi, reservasi space), reload, dan periksa ulang CLS via observer — pastikan nilai turun mendekati 0.
javascript
// Helper: hitung CLS final dan tampilkan ringkasan function summarizeCLS() { const entries = performance.getEntriesByType('layout-shift'); const unexpected = entries.filter(e => !e.hadRecentInput); const total = unexpected.reduce((sum, e) => sum + e.value, 0); const rating = total <= 0.1 ? 'Good' : total <= 0.25 ? 'Needs Improvement' : 'Poor'; console.table( unexpected.map(e => ({ waktu: `${e.startTime.toFixed(0)}ms`, score: e.value.toFixed(4), sumber: e.sources?.[0]?.node?.tagName ?? 'unknown', })) ); console.log(`Total CLS: ${total.toFixed(4)} — ${rating}`); } // Jalankan setelah halaman selesai load window.addEventListener('load', () => setTimeout(summarizeCLS, 3000));
⚠️CLS tidak selalu muncul saat testing

CLS yang dilaporkan di lapangan bisa tidak muncul saat kamu testing secara manual. Ada beberapa alasan: halaman ter-cache sehingga font dan gambar sudah ada; koneksi lebih cepat sehingga resource datang sebelum render; atau shift hanya terjadi saat user men-scroll ke area tertentu. Selalu andalkan field data (CrUX, RUM) sebagai sumber kebenaran, dan gunakan throttling agresif saat reproduksi lokal.

💡Pro Tip: Gunakan Lighthouse CI

Integrasikan Lighthouse CI ke pipeline CI/CD untuk menangkap regresi CLS sebelum deploy ke production. Konfigurasi threshold: assert: { metrics: { cumulativeLayoutShift: ["warn", { maxNumericValue: 0.1 }] } }. Regresi CLS sering muncul dari dependency update (carousel library baru, versi widget chat yang diperbarui) yang tidak terlihat di code review biasa.

📝Yang baru kamu pelajari

Debug CLS dimulai dari field data (PageSpeed Insights / CrUX), bukan langsung dari lab. Gunakan PerformanceObserver dengan type: ‘layout-shift’ untuk melihat elemen spesifik yang bergerak, dan gunakan Performance panel DevTools dengan Layout Shift Regions untuk visualisasi real-time.

06

Ringkasan

Yang wajib menempel dari chapter ini

CLS bukan soal seberapa cepat halaman dimuat, melainkan seberapa stabil tata letaknya selama pengguna berinteraksi — dan memperbaikinya berarti menghilangkan elemen kejutan: konten yang tiba-tiba menggeser apa yang sudah ada di layar.

Kita telah menelusuri CLS dari akar matematis hingga perbaikan konkret. Formula impact fraction × distance fraction menjelaskan mengapa gambar besar yang bergerak sedikit bisa menyumbang CLS lebih tinggi dari gambar kecil yang bergerak jauh. Session window menjelaskan mengapa Google tidak sekadar menjumlahkan semua shift, tapi mengambil periode buruk terpanjang.

Enam penyebab utama yang kita bahas — gambar tanpa dimensi, FOUT dari font swap, iklan yang diinjeksi, cookie banner, skeleton loader yang tidak akurat, dan animasi berbasis layout properties — semuanya dapat diperbaiki tanpa mengorbankan desain atau fungsionalitas. Kuncinya adalah mendeklarasikan ukuran di depan (sebelum konten datang), memosisikan elemen overlay di luar alir dokumen, dan menggunakan transform sebagai pengganti properti layout untuk animasi.

Yang Wajib Menempel

  • CLS = nilai session window terbesar, dihitung dari impact_fraction × distance_fraction per unexpected shift. Target: ≤ 0.1.
  • ”Unexpected” = shift yang terjadi lebih dari 500ms setelah interaksi user. Shift akibat klik/tap tidak dihitung ke CLS.
  • Setiap <img> wajib memiliki atribut width dan height — browser menggunakannya sebagai petunjuk aspek rasio untuk menyisihkan ruang sebelum gambar diunduh.
  • FOUT (font-display: swap) menyebabkan CLS ketika ukuran font custom berbeda dari fallback. Kurangi dengan preload font, font-display: optional, atau size-adjust descriptor.
  • Konten dinamis (iklan, cookie banner, widget) tidak boleh menginjeksi ke alir dokumen tanpa mereservasi ruang terlebih dahulu. Gunakan min-height pada container, atau position: fixed untuk overlay.
  • Animasikan hanya dengan transform dan opacity — keduanya tidak memicu layout recalculation dan tidak berkontribusi ke CLS. Hindari menganimasikan top, left, margin, width, atau height.
  • Debug CLS: mulai dari field data (CrUX / PSI), bukan lab. Gunakan PerformanceObserver dengan type: ‘layout-shift’ dan aktifkan Layout Shift Regions di DevTools Performance panel untuk menemukan elemen penyebab.

Di Chapter 6 kita membahas Time to First Byte (TTFB) dan First Contentful Paint (FCP) — metrik yang mengukur seberapa cepat browser menerima byte pertama dari server dan mulai menggambar sesuatu di layar, serta bagaimana keduanya memengaruhi pengalaman loading keseluruhan.