Panduan lengkap memperbaiki CLS (Cumulative Layout Shift) di bawah 0.1. Diagnosis dengan Chrome DevTools, teknik perbaikan gambar, font, iklan, dan monitoring RUM production.
CLS (Cumulative Layout Shift) adalah metrik Core Web Vitals yang mengukur seberapa sering elemen halaman berpindah posisi secara tak terduga selama proses loading. Nilai yang baik ada di bawah 0.1, dan mencapainya butuh reservasi ruang untuk gambar, font, iklan, serta konten dinamis sebelum semuanya dirender. Panduan ini membahas cara mengidentifikasi sumber layout shift memakai Chrome DevTools dan Web Vitals JS, lalu menerapkan perbaikan konkret untuk setiap penyebab umum. Kita juga bahas monitoring CLS di production dengan Real User Monitoring (RUM), supaya Anda bisa memantau skor pengguna nyata, bukan cuma lab data.
Nilai CLS "Good" adalah ≤ 0.1 pada persentil ke-75 pengguna real; "Needs Improvement" 0.1–0.25; "Poor" > 0.25.
Sejak Juni 2021 CLS memakai pendekatan session window dengan durasi maksimum 5 detik supaya tidak terus menumpuk pada halaman panjang.
Penyebab utama CLS: gambar/iframe tanpa dimensi eksplisit, web font tanpa size-adjust, iklan yang menyisipkan konten, dan animasi non-transform.
CSS aspect-ratio, content-visibility: auto + contain-intrinsic-size, dan font-display: optional adalah tiga teknik paling efektif untuk menurunkan CLS.
Field data (CrUX/RUM) selalu berbeda dari lab data. Gunakan library web-vitals untuk mengumpulkan CLS dari pengguna nyata.
Interaksi pengguna (klik, tap, keypress) memberi jeda 500ms sebelum shift dihitung, jadi animasi keyboard bukan penyebab CLS.
Apa itu CLS dan bagaimana cara menghitungnya?
Cumulative Layout Shift mengukur ketidakstabilan visual halaman. Setiap kali elemen yang sudah terlihat berpindah posisi tanpa dipicu oleh interaksi pengguna, browser menghitung layout shift score untuk shift tersebut. Score satu shift dihitung dengan rumus impact fraction × distance fraction. Impact fraction adalah proporsi viewport yang tercakup elemen yang bergeser (union dari posisi lama dan baru), sedangkan distance fraction adalah jarak pergeseran terbesar dibagi dimensi viewport yang relevan.
Contoh sederhana: sebuah elemen menempati 50% viewport dan bergeser 25% ke bawah. Impact fraction ≈ 0.75 (50% awal + 25% baru terisi), distance fraction = 0.25, jadi layout shift score = 0.1875. Sudah melebihi threshold "Good" hanya dari satu event saja. Skor CLS final adalah jumlah dari session window terbesar, di mana window baru dimulai kalau ada jeda 1 detik antar shift, atau durasi window mencapai 5 detik. Definisi resmi beserta diagramnya bisa Anda pelajari di dokumentasi CLS web.dev.
Perubahan session-window ini penting. Sebelum 2021, halaman scroll-panjang seperti forum bisa mengumpulkan CLS mendekati 1.0 hanya karena akumulasi. Sekarang, cuma "sesi shift" terburuk yang dihitung, sehingga optimasi terfokus pada momen kritis (biasanya beberapa detik pertama setelah load, atau saat konten lazy-loaded mulai muncul).
Berapa nilai CLS yang baik di 2026?
Google memakai tiga tingkat threshold untuk CLS, dan angkanya tidak berubah sejak metrik ini diperkenalkan: Good ≤ 0.1, Needs Improvement 0.1–0.25, dan Poor > 0.25. Threshold ini dievaluasi pada persentil ke-75 dari sesi pengguna real yang dikumpulkan Chrome User Experience Report (CrUX). Artinya, agar halaman "lulus" CLS, minimal 75% pengguna real Anda harus mengalami CLS ≤ 0.1 dalam periode rolling 28 hari.
Data field 2026 menunjukkan hanya sekitar 62% halaman web mencapai status "Good" untuk CLS. Masih lebih baik dibanding LCP (54%) dan INP (55%), tapi banyak site yang lolos di lab justru gagal di field karena variasi network, font caching, dan pengalaman first-visit vs return-visit. Halaman yang mendapat status "Poor" akan mendapat penalti ranking di Google Search sesuai dokumentasi Core Web Vitals di Google Search Central.
Yang perlu dicatat: CLS diukur untuk seluruh lifetime halaman sampai user meninggalkannya (unload, bfcache eviction, atau tab close). Halaman single-page application (SPA) dengan navigasi client-side tetap dianggap sebagai satu "page view" oleh browser. Ini bikin SPA rentan CLS tinggi kalau transisi route menyisipkan konten tanpa reservasi ruang. Saya pernah kena masalah ini waktu ship sebuah dashboard Nuxt, dan skor CLS di p75 melonjak tajam hanya karena satu banner notifikasi yang muncul setelah client hydrate.
Apa penyebab utama CLS tinggi?
Berdasarkan analisis HTTP Archive 2026, lima penyebab paling umum CLS tinggi adalah: (1) gambar tanpa atribut width dan height, (2) iklan atau widget pihak ketiga yang menyisipkan konten setelah render awal, (3) web font yang men-swap dari fallback ke custom font dengan metrik berbeda (FOIT/FOUT), (4) konten dinamis seperti banner cookie atau notifikasi, dan (5) animasi non-composited yang mengubah properti seperti top, height, atau margin alih-alih transform.
Pola tersembunyi yang sering terlewat: hydration mismatch di framework React/Vue/Svelte. Waktu server render menghasilkan markup yang berbeda dari client render (biasanya karena locale, feature flag, atau device detection), React akan mengganti seluruh subtree. Kalau ukuran render baru berbeda, hasilnya layout shift yang cukup besar. Teknik seperti useSyncExternalStore dan Suspense boundaries bisa mengisolasi mismatch tersebut. Pelajari lebih lanjut di panduan optimasi INP untuk framework modern, yang membahas hydration secara mendalam.
Penyebab lain yang jarang dibahas adalah scroll anchoring. Browser modern akan mencoba mempertahankan posisi visual saat konten disisipkan di atas viewport, tapi kalau CSS Anda punya overflow-anchor: none, atau shift terjadi di dalam viewport, browser tidak bisa mengkompensasinya. Cek nilai overflow-anchor di reset CSS Anda. Normalize.css versi lama pernah mematikan feature ini secara default, dan itu jadi sumber CLS senyap.
Bagaimana cara mengukur CLS di production?
Ada dua sumber data untuk CLS: lab data (Lighthouse, PageSpeed Insights, WebPageTest) dan field data (CrUX, Real User Monitoring). Lab data menjalankan halaman di environment terkontrol dan hanya menangkap CLS selama load, sedangkan CLS spec mengukur seluruh lifetime halaman. Karena itu, lab CLS hampir selalu lebih rendah dari field CLS. Jadi kalau Anda mau tahu skor pengguna nyata, field data wajib dikumpulkan.
Cara paling praktis adalah pakai library web-vitals dari GoogleChrome (v4+) yang mendukung onCLS callback dan berjalan setiap ada perubahan skor.
// npm install web-vitals@4
import { onCLS } from 'web-vitals';
// Callback dipanggil setiap kali skor CLS berubah
// { reportAllChanges: true } mengirim setiap update, bukan hanya saat page hidden
onCLS(
(metric) => {
// metric.value adalah skor CLS saat ini
// metric.entries berisi array LayoutShift entries yang berkontribusi
// metric.attribution berisi elemen dan waktu shift terburuk
const payload = {
name: 'CLS',
value: metric.value,
rating: metric.rating,
largest_shift_target: metric.attribution?.largestShiftTarget,
largest_shift_time: metric.attribution?.largestShiftTime,
url: location.pathname,
};
// Kirim via sendBeacon agar tidak memblokir unload
navigator.sendBeacon('/api/rum', JSON.stringify(payload));
},
{ reportAllChanges: false }
);
Kunci pentingnya: gunakan opsi attribution dari web-vitals/attribution submodule agar payload menyertakan selector CSS dari elemen yang bergeser. Tanpa data ini, Anda hanya akan tahu bahwa CLS tinggi, tapi tidak tahu elemen mana yang menyebabkannya di production. (Ini pengalaman pahit yang saya alami di proyek pertama yang saya integrasi RUM sendiri.)
Perbaikan CLS untuk gambar dan iframe
Aturan pertama: setiap tag <img> dan <iframe> harus memiliki atribut width dan height eksplisit. Browser modern (Chrome 79+, Firefox 71+, Safari 14+) memakai atribut ini untuk menghitung aspect-ratio secara implisit, sehingga slot ruang direservasi sebelum gambar dimuat. Untuk responsive images, kombinasikan dengan CSS width: 100%; height: auto;. Atribut HTML memberikan aspect ratio, CSS mengontrol ukuran final.
<!-- Baik: aspect ratio 16:9 dihitung dari 1600:900 -->
<img
src="/hero.avif"
width="1600"
height="900"
alt="Product screenshot"
style="width: 100%; height: auto;"
>
<!-- Untuk gambar dengan dimensi tidak diketahui saat build (mis. CMS user upload) -->
<div style="aspect-ratio: 16 / 9; background: #f5f5f5;">
<img src="/dynamic.jpg" alt="" style="width: 100%; height: 100%; object-fit: cover;">
</div>
Untuk gambar responsive dengan srcset yang punya aspect ratio berbeda per breakpoint, pakai properti CSS aspect-ratio yang berbeda dengan media query. Hindari solusi lama seperti "padding-bottom hack" (padding-bottom: 56.25%). Teknik itu masih jalan, tapi tidak sekomposabel aspect-ratio dan lebih sulit dibaca. Iframe embed seperti YouTube atau Twitter juga rentan CLS kalau tidak dibungkus dengan container ber-aspect-ratio.
Optimasi web fonts dengan size-adjust
Web font menyebabkan CLS ketika fallback font (misalnya Arial) memiliki metrik berbeda dari custom font (misalnya Inter). Setelah font custom dimuat, karakter mendadak lebih tinggi/pendek/lebar, jadi baris teks flow ulang. Solusi modern (2026) memakai kombinasi font-display dan size-adjust/ascent-override/descent-override pada @font-face declaration.
/* Font fallback dengan metric override yang match Inter */
@font-face {
font-family: 'InterFallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
/* Custom font dengan swap yang tidak menggeser layout */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2-variations');
font-display: swap;
font-weight: 100 900;
}
body {
font-family: 'Inter', 'InterFallback', system-ui, sans-serif;
}
Nilai override yang tepat harus dihitung untuk setiap kombinasi font. Tool Capsize dari SEEK mengotomasi kalkulasi ini dengan membaca metrik dari file font. Alternatif zero-config: pakai font-display: optional, yang memberi browser 100ms untuk memuat font. Kalau belum siap, browser akan tetap di fallback dan menggunakan custom font di navigasi berikutnya (setelah cached). Trade-off-nya: first-time visitor tidak akan melihat custom font, tapi CLS-nya nol.
Framework seperti Next.js 15 (next/font) dan Nuxt (@nuxt/fonts) sudah mengotomasi size-adjust dengan menghitung metric override saat build. Kalau Anda pakai framework tersebut, cukup pastikan opsi adjustFontFallback tidak dimatikan.
Menangani ads, embeds, dan konten yang disisipkan
Iklan display (Google AdSense, banner) adalah kontributor CLS terbesar untuk site media. Karena ukuran iklan tidak diketahui sampai bidding selesai, iklan yang lolos filter viewability sering muncul mendadak dan menggeser konten. Solusinya: selalu reservasi slot dengan ukuran minimum, walaupun iklan mungkin tidak muncul.
<!-- Slot iklan dengan min-height untuk ukuran terbesar yang mungkin -->
<div class="ad-slot" style="min-height: 250px; display: flex; align-items: center; justify-content: center;">
<!-- AdSense akan mengisi dari sini -->
<ins class="adsbygoogle"
style="display:block; width:300px; height:250px"
data-ad-client="ca-pub-xxx"
data-ad-slot="yyy"></ins>
</div>
Untuk konten yang muncul berdasarkan interaksi (misalnya modal, tooltip), pakai CSS position: fixed atau position: absolute. Elemen di layer positioning berbeda tidak menghitung CLS, karena mereka tidak menggeser flow document. Cookie consent banner adalah kasus klasik. Alih-alih menyisipkan di atas hero, pakai bottom sheet fixed atau bungkus dengan overlay modal.
Konten dinamis dari A/B testing atau personalization: kalau Anda perlu hydrate konten yang berbeda berdasarkan user data, gunakan skeleton placeholder dengan tinggi yang sama sebelum konten final di-render. Framework seperti Next.js 15 mendukung <Suspense fallback={<Skeleton />}> untuk pola ini. Untuk pengaruh personalization terhadap performa web secara umum, silakan baca panduan optimasi TTFB dan strategi edge rendering.
Menggunakan content-visibility dan contain-intrinsic-size
CSS content-visibility: auto memberi tahu browser untuk melewati rendering elemen yang berada jauh dari viewport, sehingga mengurangi waktu paint awal. Tapi properti ini punya efek samping CLS: karena browser tidak me-layout konten sampai dibutuhkan, elemen di-collapse ke tinggi 0 hingga scroll mendekat, lalu tiba-tiba ter-expand. Solusinya contain-intrinsic-size, yang memberikan hint ukuran default.
/* Kartu artikel dengan lazy rendering + reserve size */
.article-card {
content-visibility: auto;
contain-intrinsic-size: auto 400px; /* estimated height */
}
/* Untuk grid produk dengan tinggi bervariasi */
.product-item {
content-visibility: auto;
contain-intrinsic-size: 300px 500px; /* width height */
}
Nilai auto di depan memberi tahu browser untuk "mengingat" ukuran actual setelah pertama kali dirender, jadi scroll berikutnya memakai ukuran nyata. Fitur ini didukung di semua browser evergreen sejak awal 2024 (Chrome 85+, Safari 18+, Firefox 125+). Kombinasikan dengan view-transition-name untuk transisi halus antar route SPA. Pattern ini juga membantu optimasi LCP dan mengurangi element render delay.
Debugging CLS di Chrome DevTools
Chrome DevTools menyediakan tiga alat khusus untuk debug CLS: Performance panel dengan Web Vitals track, Rendering panel dengan Layout Shift Regions overlay, dan Performance Insights (deprecated 2025, digantikan Performance panel yang enhanced).
Langkah-langkah debug:
Buka DevTools → Performance panel
Aktifkan checkbox "Web Vitals" di header rekaman
Refresh halaman dengan Ctrl+Shift+R (hard reload untuk mendapat first-load scenario)
Cari red diamond markers di track "Timings". Setiap marker adalah layout shift event
Klik marker untuk melihat detail: "Moved from" (rect awal), "Moved to" (rect akhir), dan node HTML yang berpindah
Bandingkan dengan "Interactions" track. Kalau shift terjadi <500ms setelah user interaction, tidak dihitung ke CLS (dianggap intentional)
Untuk visualisasi real-time saat browsing, buka Rendering panel (Cmd/Ctrl+Shift+P → "Show Rendering") dan aktifkan "Layout Shift Regions". Browser akan menampilkan overlay biru pada area yang mengalami shift, sehingga memudahkan identifikasi visual selama development.
Monitoring CLS jangka panjang dengan RUM
Setelah CLS diperbaiki di production, monitoring berkelanjutan itu wajib karena regressi gampang terjadi: developer menambah komponen baru, third-party script berubah, atau A/B test baru dirilis. Setup RUM minimum meliputi: agregasi CLS per URL pattern, dimensi device type (mobile vs desktop), dan attribution ke element selector.
Sistem seperti Cloudflare Web Analytics mengumpulkan Core Web Vitals tanpa cookie dan tanpa tambahan script berat. Untuk kontrol lebih detail, kirim data ke pipeline analytics milik Anda sendiri (BigQuery, ClickHouse) dengan schema seperti ini:
-- Schema untuk RUM CLS event
CREATE TABLE web_vitals_cls (
timestamp TIMESTAMP,
session_id STRING,
url_pattern STRING, -- '/product/[id]' bukan '/product/123'
device_type STRING, -- 'mobile' | 'desktop' | 'tablet'
connection_type STRING, -- '4g' | '3g' | 'wifi'
cls_value FLOAT,
cls_rating STRING, -- 'good' | 'needs-improvement' | 'poor'
largest_shift_element STRING, -- CSS selector
largest_shift_time INT, -- ms sejak navigation start
browser STRING,
country STRING
);
-- Query: p75 CLS per URL pattern selama 7 hari terakhir
SELECT
url_pattern,
APPROX_QUANTILES(cls_value, 100)[OFFSET(75)] AS p75_cls,
COUNT(*) AS sessions
FROM web_vitals_cls
WHERE timestamp >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 7 DAY)
GROUP BY url_pattern
HAVING sessions >= 100
ORDER BY p75_cls DESC;
Set alert kalau p75 CLS suatu URL pattern melewati 0.1 selama 24 jam berturut-turut. Padukan dengan git blame otomasi untuk mengidentifikasi commit yang jadi penyebabnya. Metode ini juga jalan untuk monitoring performa Speculation Rules API. Perlu diperhatikan, halaman yang di-prerender punya CLS yang berbeda karena render terjadi sebelum interaksi.
Checklist implementasi CLS < 0.1
Audit semua <img> dan <iframe>, lalu tambahkan atribut width/height eksplisit
Setup fallback font dengan size-adjust yang match custom font
Reservasi slot minimum untuk iklan dan widget pihak ketiga
Pindahkan cookie banner ke posisi fixed di bawah viewport
Tambahkan skeleton placeholder untuk konten hydration/A/B test
Ganti animasi yang mengubah top/left/width/height ke transform
Tambahkan contain-intrinsic-size untuk setiap elemen dengan content-visibility: auto
Deploy web-vitals library dengan attribution ke pipeline RUM
Setup alert p75 CLS per URL pattern di dashboard analytics
Jalankan Lighthouse CI di setiap PR untuk mencegah regressi
Pertanyaan yang Sering Diajukan
Apakah CLS mempengaruhi ranking SEO?
Ya. Sejak Page Experience Update Juni 2021, Core Web Vitals (termasuk CLS) menjadi faktor ranking Google Search untuk semua platform (mobile dan desktop). Halaman dengan CLS di kategori "Poor" (> 0.25) mendapat sinyal negatif, sedangkan halaman "Good" (≤ 0.1) memenuhi threshold minimum. Konten tetap prioritas utama; CLS berperan sebagai tie-breaker antara halaman dengan relevansi konten yang mirip.
Mengapa CLS di Lighthouse berbeda dengan PageSpeed Insights?
Lighthouse mengukur lab CLS, yaitu skor selama halaman diload di environment terkontrol. PageSpeed Insights menampilkan lab CLS dan field CLS dari CrUX (data pengguna real 28 hari terakhir). Field CLS hampir selalu lebih tinggi karena mencakup seluruh lifetime halaman, termasuk shift dari interaksi setelah load. Untuk keputusan optimasi, gunakan field CLS.
Bagaimana cara mencegah CLS dari iklan yang ukurannya tidak diketahui?
Reservasi slot dengan min-height yang sesuai ukuran iklan terbesar yang mungkin muncul, atau pakai tinggi tetap berdasarkan ukuran iklan paling umum di slot tersebut (misalnya 250px untuk MPU). Kalau iklan tidak dilayani, slot kosong tetap lebih baik daripada layout shift. Untuk header/sticky ads, pakai position: fixed supaya tidak menggeser konten.
Apakah animasi CSS menyebabkan CLS?
Tergantung properti yang dianimasikan. Animasi transform dan opacity tidak menyebabkan CLS karena ditangani di composite thread tanpa mengubah layout. Animasi properti seperti height, width, margin, top, atau left menyebabkan reflow dan berkontribusi ke CLS. Selalu gunakan transform: translate() alih-alih top/left, dan transform: scale() alih-alih width/height.
Berapa lama waktu perbaikan CLS terlihat di Google Search Console?
Data CrUX yang dipakai Google Search Console adalah rolling 28-day window. Setelah perbaikan di-deploy, dibutuhkan minimal 28 hari agar seluruh window terisi data baru. Namun, tren biasanya sudah kelihatan dalam 3-7 hari kalau traffic halaman cukup tinggi. Gunakan RUM sendiri untuk validasi cepat dalam hitungan jam, lalu konfirmasi lewat CrUX/GSC untuk baseline resmi.
Apakah bfcache mempengaruhi pengukuran CLS?
Ya, secara positif. Ketika halaman dipulihkan dari back/forward cache (bfcache), tidak ada layout shift karena state DOM sudah lengkap. Web-vitals library v3+ tidak me-reset CLS saat restore dari bfcache, jadi nilai yang sudah dikumpulkan tetap valid. Pastikan halaman Anda bfcache-eligible (hindari unload listener dan Cache-Control: no-store) supaya CLS lebih rendah pada navigasi kembali.
TTFB adalah fondasi performa web modern—menentukan seberapa cepat LCP, FCP, dan keseluruhan pengalaman pengguna. Pelajari cara mengukur, mendiagnosis, dan mengurangi TTFB hingga di bawah 200ms melalui optimasi server, caching multi-layer, edge computing, dan tuning database.
Pelajari cara mengoptimasi LCP dengan membedah 4 sub-parts-nya: TTFB, Resource Load Delay, Resource Load Duration, dan Element Render Delay. Panduan lengkap dengan kode implementasi dan checklist praktis untuk Core Web Vitals 2026.
Panduan lengkap Speculation Rules API untuk membuat navigasi web terasa instan. Pelajari perbedaan prefetch vs prerender, parameter eagerness, implementasi di WordPress, Next.js, dan Nuxt.js, serta dampaknya terhadap Core Web Vitals.