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.
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.
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:
plaintextlayout_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.
| Kategori | Nilai CLS | Status |
|---|---|---|
| Good | ≤ 0.1 | Halaman stabil secara visual |
| Needs Improvement | 0.1 – 0.25 | Ada shift yang mengganggu |
| Poor | > 0.25 | Halaman sangat tidak stabil |
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.
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.
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 Elemen | Penyebab CLS | Fix |
|---|---|---|
<img> | Tidak ada width / height | Tambah atribut, pakai height: auto di CSS |
<video> | Poster image tanpa dimensi | Set width dan height pada elemen <video> |
<iframe> | Tidak ada tinggi tetap | Wrapper dengan aspect-ratio CSS |
| CSS background-image | Container tanpa dimensi tetap | Set aspect-ratio atau tinggi eksplisit pada container |
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).
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.
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-display | Perilaku | CLS Risk | Teks Terlihat? |
|---|---|---|---|
auto | Tergantung browser (biasanya block) | Rendah | Tidak (sementara) |
block | FOIT: teks disembunyikan hingga 3 detik | Sangat Rendah | Tidak (sementara) |
swap | FOUT: fallback lalu ganti ke custom | Tinggi | Ya |
fallback | Block singkat (100ms), lalu swap jika sudah siap | Sedang | Ya (setelah 100ms) |
optional | Pakai custom hanya jika sudah cache; jika belum, pakai fallback seterusnya | Sangat Rendah | Ya |
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.
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”.
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.
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.
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 CSS | Trigger Layout? | Trigger Paint? | Trigger Composite? | CLS Risk |
|---|---|---|---|---|
transform | Tidak | Tidak | Ya | Tidak ada |
opacity | Tidak | Tidak | Ya | Tidak ada |
top / left | Ya | Ya | Ya | Tinggi |
margin / padding | Ya | Ya | Ya | Tinggi |
width / height | Ya | Ya | Ya | Tinggi |
background-color | Tidak | Ya | Ya | Tidak 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 --> EDiagram 3. Alur keputusan memilih properti CSS untuk animasi agar tidak memicu layout shift.
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.
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.
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:
javascriptimport { 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:
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
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 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.
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.
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.
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_fractionper 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 atributwidthdanheight— 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, atausize-adjustdescriptor. - Konten dinamis (iklan, cookie banner, widget) tidak boleh menginjeksi ke alir dokumen tanpa mereservasi ruang terlebih dahulu. Gunakan
min-heightpada container, atauposition: fixeduntuk overlay. - Animasikan hanya dengan
transformdanopacity— keduanya tidak memicu layout recalculation dan tidak berkontribusi ke CLS. Hindari menganimasikantop,left,margin,width, atauheight. - Debug CLS: mulai dari field data (CrUX / PSI), bukan lab. Gunakan
PerformanceObserverdengantype: ‘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.