Web Performance · Part 7 — Optimizing CLS
Layout jumps happen when content is inserted without reserving space. Set image/iframe dimensions, reserve space for dynamic content and ads, handle font swap, and use transform instead of layout changes.
CLS (≤ 0.1) đo mức độ bố cục nhảy bất ngờ. Khác với LCP/INP (về tốc độ), CLS về sự ổn định. Nguyên nhân gần như luôn là một: nội dung được chèn vào mà không chừa chỗ trước, đẩy những thứ xung quanh đi. Tin tốt: phần lớn CLS sửa được triệt để bằng cách đặt chỗ trước.
1. Ảnh & video không có kích thước
Thủ phạm số một. Khi ảnh chưa tải, trình duyệt không biết nó cao bao nhiêu nên dành 0px; tải xong, ảnh “bung ra” đẩy nội dung bên dưới xuống.
<!-- ❌ no dimensions → browser allocates 0px → jumps when loaded -->
<img src="/photo.jpg" alt="..." />
<!-- ✅ declare width/height → browser computes aspect-ratio and reserves space -->
<img src="/photo.jpg" alt="..." width="800" height="600" />
Mấu chốt hiện đại: chỉ cần khai báo width/height (thuộc tính HTML), trình duyệt tự tính aspect-ratio và giữ chỗ đúng — kể cả khi CSS làm ảnh co giãn responsive:
img {
max-width: 100%;
height: auto; /* preserve ratio; paired with HTML width/height → no jump */
}
Với ảnh chưa biết tỉ lệ, dùng aspect-ratio trong CSS để giữ khung:
.thumb {
aspect-ratio: 16 / 9; /* reserve the correct ratio frame before the image loads */
}
Áp dụng tương tự cho <video>, <iframe> (embed YouTube, bản đồ) — luôn đặt kích thước/khung.
Nếu <picture> dùng art direction với các crop khác tỉ lệ, width/height trên <img> không đủ cho mọi breakpoint. Khai kích thước tương ứng trên từng <source> để browser giữ đúng chỗ trước khi chọn ảnh:
<picture>
<source media="(max-width: 799px)" srcset="/hero-crop.jpg" width="480" height="600" />
<source media="(min-width: 800px)" srcset="/hero-wide.jpg" width="1200" height="600" />
<img src="/hero-wide.jpg" width="1200" height="600" alt="..." />
</picture>
2. Nội dung chèn động: dành không gian trước
Banner thông báo, “đọc thêm”, nội dung tải bất đồng bộ — nếu chèn vào trên nội dung người dùng đang xem sẽ đẩy mọi thứ xuống.
Nguyên tắc:
- Dành sẵn không gian (min-height) cho khu vực sẽ có nội dung động, để khi nội dung về không đẩy gì.
- Đừng chèn nội dung phía trên nội dung hiện có trừ khi do người dùng chủ động. CLS bỏ qua shift có
hadRecentInputtrong vòng 500ms sau một số tương tác rời rạc như click/tap/keypress; scroll, drag và hover không được miễn tự động. - Skeleton có kích thước đúng với nội dung thật — skeleton cao 100px rồi nội dung cao 300px vẫn gây shift.
/* Reserve space for the banner so it doesn't push content when it appears */
.notification-slot {
min-height: 60px;
}
3. Quảng cáo & embed: khung cố định
Quảng cáo là nguồn CLS khét tiếng vì kích thước thay đổi và tải muộn. Hãy dành trước kích thước theo breakpoint và loại slot; nếu nhiều creative có chiều cao khác nhau, dùng min-height theo kích thước lớn nhất hợp lý hoặc tránh làm slot co lại sau khi đã hiển thị:
.ad-slot {
min-height: 250px; /* match ad unit size; reserve even before ad loads */
display: flex;
align-items: center;
justify-content: center;
}
4. Font swap gây shift (FOUT)
Khi font tùy chỉnh tải xong và thay font hệ thống, kích thước chữ thay đổi nhẹ → text “xô” lại → CLS. Đây là đánh đổi với LCP (Phần 10 bàn kỹ). Cách giảm shift:
size-adjust/ascent-overridetrong@font-faceđể font hệ thống fallback khớp metric với font tùy chỉnh, làm bước swap gần như không đổi kích thước:
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107%; /* illustrative only: derive this from the actual font pair */
ascent-override: 90%;
}
- Preload có chọn lọc font thật sự critical (Phần 4) để giảm thời gian dùng fallback. Preload không tự xóa CLS: nếu metric hai font lệch, swap sớm vẫn có thể bị tính.
- Nếu thương hiệu không bắt buộc phải swap trong lượt đầu, cân nhắc
font-display: optionalđể tránh late swap.
Đây là điểm tinh tế:
font-display: swaptốt cho cảm nhận tốc độ (text hiện ngay) nhưng có thể gây CLS nếu fallback lệch metric.size-adjustvà các metric override giúp giảm rủi ro, nhưng phải tính từ đúng cặp font và đo lại.
5. Animation: dùng transform, không đổi layout
Animate các thuộc tính layout (top, left, width, height, margin) làm phần tử dịch chuyển trong dòng layout → có thể tính vào CLS và gây reflow mỗi frame. Animate bằng transform thì không:
/* ❌ changing top shifts layout + may count toward CLS */
.toast { transition: top 0.3s; }
/* ✅ transform avoids layout shifts; compositing still depends on the browser/element */
.toast { transition: transform 0.3s; }
(Đây cũng là bài học của RAIL/Animation ở Phần 1 và FLIP ở series thuật toán Phần 11.)
6. Tìm thủ phạm CLS cụ thể
CLS khó vì shift xảy ra chớp nhoáng. Cách định vị:
- Tab Performance (Phần 3): mỗi shift hiện một vạch “Layout Shift”; click để xem phần tử nào nhảy.
LayoutShiftAPI (Phần 2): logentry.sourcesđể biết node nào dịch chuyển.
if (PerformanceObserver.supportedEntryTypes.includes('layout-shift')) {
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
const e = entry as LayoutShift;
if (!e.hadRecentInput && e.value > 0.01) {
// sources are nodes that moved—not necessarily the root cause.
console.log('Shift', e.value.toFixed(3), e.sources?.map((s) => s.node));
}
}
}).observe({ type: 'layout-shift', buffered: true });
}
LayoutShift/entry.sources chưa có độ phủ đồng đều ở mọi browser; dùng DevTools và web-vitals/attribution để chẩn đoán production, rồi kiểm tra thay đổi DOM ngay trước các node bị dịch chuyển.
7. Checklist CLS
- Mọi
<img>/<video>/<iframe>có width/height hoặc aspect-ratio? - Khu vực nội dung động có min-height chừa sẵn?
- Quảng cáo/embed bọc trong container cố định?
- Font có chiến lược
font-display, preload có chọn lọc và fallback khớp metric (size-adjust)? - Animation chỉ dùng transform/opacity, không đổi thuộc tính layout?
- Skeleton đúng kích thước với nội dung thật?
Tóm tắt
- CLS = cụm dịch chuyển bố cục bất ngờ; nguyên nhân thường là nội dung/tài nguyên tới muộn mà không chừa chỗ.
- Ảnh/video/iframe: luôn khai width/height hoặc aspect-ratio.
- Nội dung động/quảng cáo: dành min-height và container cố định; đừng chèn phía trên nội dung đang xem.
- Font swap: chọn
font-displayphù hợp, dùng fallback khớp metric và chỉ preload font critical; đo lại vì không kỹ thuật nào tự đảm bảo CLS bằng 0. - Animate bằng transform, không đổi thuộc tính layout.
Phần tiếp theo: JavaScript performance — bundle size, code splitting, tree shaking và lazy load.