KIếN THứC WEBSITE › PERFORMANCE

Fix CLS — 5 bước detect và 7 fix patterns cho layout shift

Fix CLS — 5 bước detect và 7 fix patterns cho layout shift 2026

CLS (Cumulative Layout Shift) đo tổng mức dịch chuyển bố cục bất ngờ trong phiên — phần tử nhảy vị trí khiến người dùng click nhầm. Ngưỡng pass của Google là dưới 0,1.

CLS là gì và tính ra sao

CLS là gì và tính ra sao
CLS là gì và tính ra sao

CLS đo tổng dịch chuyển bố cục của tất cả phần tử không được người dùng kích hoạt. Điểm số = tổng (impact fraction × distance fraction) cho mỗi lần dịch chuyển, nhóm trong cửa sổ 5s sau tương tác.

Nếu người dùng click và gây ra dịch chuyển, lần đó không được tính vào CLS. Chỉ những dịch chuyển bất ngờ — xảy ra mà không có hành động của người dùng — mới bị tính.

Impact fraction và distance fraction

  • Impact fraction: tỷ lệ khung nhìn bị ảnh hưởng (vị trí cũ + vị trí mới). Ví dụ: ảnh chiếm 50% khung nhìn dịch chuyển xuống = impact 50%.
  • Distance fraction: khoảng cách dịch chuyển chia cho chiều cao hoặc chiều rộng lớn nhất của khung nhìn. Dịch chuyển 100px trong khung nhìn 800px cao = distance 12,5%.
  • Điểm dịch chuyển: 0,5 × 0,125 = 0,0625 cho lần dịch chuyển đó.

Ngưỡng đánh giá CLS

Điểm CLS Đánh giá Ảnh hưởng xếp hạng
0 – 0,1 Tốt (xanh) Pass Core Web Vitals
0,1 – 0,25 Cần cải thiện (vàng) WARN — không bị phạt nhưng nên sửa
Trên 0,25 Kém (đỏ) Fail — ảnh hưởng xếp hạng rõ rệt

Bước 1 — Phát hiện dịch chuyển bố cục

Bước 1 — Phát hiện dịch chuyển bố cục
Sơ đồ minh hoạ — Bước 1 — Phát hiện dịch chuyển bố cục

Phát hiện trước khi sửa. CLS thường ẩn vì trang chạy nhanh trên môi trường dev local.

Chỉ xuất hiện khi test với mạng chậm và dữ liệu production thực — thiết lập môi trường test chuẩn.

Kết hợp cả DevTools và theo dõi RUM để bắt được CLS trên nhiều loại thiết bị và mạng khác nhau.

Chrome DevTools — tab Hiệu năng và Layout Shift Regions

  1. Mở DevTools → Settings (F1) → Experiments → bật “Layout Shift Regions”.
  2. Mở tab Performance.
  3. Giả lập mạng “Slow 4G” và CPU chậm 4x.
  4. Ghi lại quá trình tải trang.
  5. Dịch chuyển bố cục được tô màu xanh trên timeline và ảnh chụp màn hình.
  6. Click vào ô xanh → xem phần tử và vị trí trước/sau dịch chuyển.

web-vitals.js — theo dõi RUM

import {onCLS} from 'web-vitals';

onCLS((metric) => {
  gtag('event', 'web_vitals', {
    metric_name: 'CLS',
    metric_value: Math.round(metric.value * 1000) / 1000,
    metric_id: metric.id,
    largest_shift_target: metric.attribution?.largestShiftTarget,
    largest_shift_time: metric.attribution?.largestShiftTime,
  });
});

PageSpeed Insights — kiểm tra nhanh

PSI hiển thị CLS từ dữ liệu CrUX 28 ngày ở tab “Origin Summary”. Mục “Avoid large layout shifts” trong phần Diagnostics liệt kê selector phần tử và điểm dịch chuyển.

Đây là điểm khởi đầu nhanh nhất trước khi dùng DevTools.

Bước 2 — Sửa CLS từ ảnh

Bước 2 — Sửa CLS từ ảnh
Bước 2 — Sửa CLS từ ảnh

Ảnh không có kích thước đặt trước là nguyên nhân CLS phổ biến nhất. Trình duyệt tải ảnh bất đồng bộ; khi ảnh tải xong, bố cục tính toán lại và đẩy các phần tử bên dưới xuống.

Đây là lỗi dễ sửa nhất và thường có tác động lớn nhất. Fix ảnh trước khi xử lý các nguyên nhân phức tạp hơn.

Đặt thuộc tính width và height

<!-- Sai: không có kích thước -->
<img src="/hero.jpg" alt="Hero">

<!-- Đúng: có width và height -->
<img src="/hero.jpg" alt="Hero" width="1920" height="1080">

Trình duyệt dùng widthheight để tính tỉ lệ khung hình, đặt trước không gian đúng ngay từ đầu. Trình duyệt hiện đại (Chrome 79+) tự áp dụng CSS aspect-ratio từ thuộc tính này.

CSS aspect-ratio cho ảnh responsive

/* Đặt trước không gian linh hoạt theo chiều rộng container */
.hero-image {
  width: 100%;
  aspect-ratio: 16 / 9; /* hoặc 4/3, 1/1 */
  object-fit: cover;
}

Thẻ picture với nhiều nguồn

<picture>
  <source media="(min-width: 768px)" srcset="/hero-desktop.webp" width="1920" height="1080">
  <source media="(min-width: 480px)" srcset="/hero-tablet.webp" width="1024" height="640">
  <img src="/hero-mobile.webp" alt="Hero" width="600" height="375">
</picture>

Đọc thêm về tối ưu ảnh tại bài WebP vs AVIF — so sánh định dạng ảnhhướng dẫn lazy load ảnh 2026.

Bước 3 — Sửa CLS từ font swap

Font swap gây FOUT (Flash of Unstyled Text) — text dùng font dự phòng, swap khi font tùy chỉnh tải xong. Nếu hai font có số liệu khác nhau, text bị tính toán lại vị trí gây CLS.

Sửa bằng size-adjustascent/descent overrides.

Kết hợp ba kỹ thuật dưới đây cho kết quả tốt nhất — dùng riêng lẻ thường chỉ giảm được một phần CLS từ font.

Self-host font và preload

<!-- Preload font quan trọng trong <head> -->
<link rel="preload" href="/fonts/inter.woff2" as="font"
      type="font/woff2" crossorigin>

<style>
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap;
  font-weight: 400 700;
  size-adjust: 100%;
  ascent-override: 90%;
  descent-override: 22%;
}
</style>

Khớp số liệu font dự phòng

/* Chuỗi dự phòng khớp số liệu với Inter */
body {
  font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

/* Công cụ đo số liệu: tools.pixelambacht.nl/fontmetrics */

Xem hướng dẫn chi tiết tại self-host Google Fontfont-display swap — giải thích đầy đủ.

Bước 4 — Sửa CLS từ nội dung động

Phần tử được chèn vào sau lần tải đầu (banner, popup, embed video, widget mạng xã hội) đẩy nội dung xuống và gây dịch chuyển. Cách sửa: đặt trước không gian hoặc chèn phía trên khung nhìn.

Nguyên tắc chung: không thể tránh chèn nội dung sau khi tải thì phải đặt trước không gian đúng kích thước.

Đặt trước không gian cho slot quảng cáo hoặc banner

/* Đặt trước 250px cho slot banner */
.ad-slot {
  min-height: 250px;
  display: block;
}
<!-- Sai: chèn sau khi JS tải xong -->
<script>document.body.innerHTML += '<div>Cookie banner</div>';</script>

<!-- Đúng: inline trong HTML, ẩn bằng CSS, JS chỉ toggle visibility -->
<div id="cookie-banner" class="hidden">...banner...</div>

Embed YouTube hoặc iframe — đặt kích thước cố định

<iframe src="https://youtube.com/embed/xxx"
        width="640" height="360"
        loading="lazy"
        title="Video"></iframe>

Dùng CSS aspect-ratio: 16/9 kết hợp với width: 100% để embed video responsive mà vẫn đặt trước không gian.

Bước 5 — Sửa CLS từ animation

Animation dùng top, left, width, height kích hoạt tính toán lại bố cục → gây CLS. Thay bằng transformopacity để chỉ dùng compositor, bỏ qua layout và paint.

Đây là lỗi thường gặp trong các theme premium dùng animation CSS không chuẩn — kiểm tra toàn bộ animation trước khi ship.

Transform thay thế top/left

/* Sai: kích hoạt tính toán lại bố cục */
.box { animation: slide 1s; }
@keyframes slide {
  from { left: 0; }
  to   { left: 100px; }
}

/* Đúng: chỉ dùng compositor, không gây CLS */
.box { animation: slide 1s; }
@keyframes slide {
  from { transform: translateX(0); }
  to   { transform: translateX(100px); }
}

Will-change cho phần tử có animation

.modal-overlay {
  will-change: transform, opacity;
  /* Trình duyệt tạo composite layer riêng cho phần tử này */
}

Kiểm tra animation trong DevTools

Mở tab Rendering trong DevTools → bật “Paint flashing”. Phần tử bị repaint sẽ tô màu xanh khi animation chạy.

Phần tử dùng transform đúng cách sẽ không bị tô màu.

Checklist CLS cho shop VN

  1. Đo CLS baseline qua PSI và ghi lại trước khi bắt đầu sửa.
  2. Chạy DevTools với “Slow 4G” để phát hiện dịch chuyển ẩn.
  3. Thêm widthheight cho tất cả ảnh trong HTML.
  4. Thêm aspect-ratio CSS cho ảnh responsive.
  5. Self-host font quan trọng và thêm preload vào <head>.
  6. Thêm font-display: swap cùng size-adjustascent-override.
  7. Đặt min-height cho tất cả slot banner và quảng cáo.
  8. Chuyển banner cookie consent sang dạng inline HTML + toggle CSS.
  9. Thêm widthheight cho tất cả iframe embed.
  10. Kiểm tra toàn bộ CSS animation — chuyển sang transformopacity.
  11. Thiết lập web-vitals.js để theo dõi CLS liên tục.
  12. Đo lại CLS sau mỗi thay đổi, so sánh với baseline.

Bài liên quan

Câu hỏi thường gặp

CLS đo khác nhau giữa Desktop và Mobile không?

Có, đo riêng biệt. Mobile thường có CLS cao hơn do font tải chậm hơn và slot quảng cáo khác kích thước.

Search Console và PSI tách thành 2 tab riêng.

Ưu tiên sửa Mobile trước vì hơn 70% traffic VN đến từ di động.

Lazy load ảnh có gây CLS không?

Không, nếu đặt widthheight đúng. Trình duyệt đặt trước không gian dù ảnh chưa tải.

Lazy load giúp cải thiện hiệu năng và không ảnh hưởng CLS khi dùng đúng cách.

Skeleton loader có giúp giảm CLS không?

Có. Skeleton render phần giữ chỗ cùng kích thước với nội dung thật — khi dữ liệu tải xong, swap không gây dịch chuyển.

Pattern phổ biến cho trang danh sách và trang quản trị.

Animation CSS phức tạp — làm sao tránh CLS?

Chỉ dùng transformopacity. Tránh animate width, height, margin, padding.

Nếu cần thay đổi kích thước, dùng scale kết hợp transform-origin.

WooCommerce mini-cart drawer gây CLS — sửa thế nào?

Mini-cart drawer phải dùng position: fixed cùng transform: translateX, không gây dịch chuyển bố cục. Plugin nào dùng append hoặc insert DOM sẽ gây CLS.

Cách sửa: chuyển sang plugin dùng overlay drawer, hoặc tắt mini-cart và dùng icon liên kết đến trang giỏ hàng.

Điểm CLS tốt trên Lighthouse nhưng fail trong CrUX — tại sao?

Lighthouse là dữ liệu lab (một lần chạy, mô phỏng), CrUX là dữ liệu thực từ người dùng với mạng và thiết bị đa dạng. Tin tưởng CrUX vì đây là chỉ số xếp hạng cuối cùng.

Dùng Lighthouse để debug, không dùng để đánh giá pass/fail thực.

Cần audit và sửa CLS toàn diện cho shop online — kích thước ảnh, font swap, đặt trước không gian cho nội dung động? Xem dịch vụ tối ưu Core Web Vitals của Web22.