Bốn lần tôi đoán sai nguồn CLS, lần thứ năm mới trúng

Ảnh bìa: CLS máy tính bàn giảm từ 0,138 xuống 0,023 sau khi tự host font và khai font dự phòng khớp metric

Bài viết do Nguyễn Nhật Ánh Dương thực hiện, biên soạn theo Chính sách nội dung của Marketing365. Cập nhật lần cuối .

Nội dung
  1. Con số bắt tôi phải đi tìm
  2. Bốn lần tôi tưởng đã bắt được thủ phạm
  3. Cái duy nhất còn lại sau khi gạch hết
  4. Tự host font: mấy chỗ dễ làm hỏng
  5. Nghiệm thu, kể cả con số đi ngược
  6. Thứ tôi mang đi khỏi vụ này

Ngày 22/07/2026, tôi nhận kết quả PageSpeed Insights mới của trang chủ marketing365.vn. Bản điện thoại 71 điểm, khá hơn hẳn lần trước. Bản máy tính bàn 89 điểm, nhưng kèm một con số tôi không hiểu nổi: CLS 0,138. Lần đo trước đó, chỉ số này bằng 0.

CLS đo mức xô lệch bố cục: nội dung đã vẽ ra màn hình rồi lại bị đẩy sang chỗ khác. Ngưỡng đạt là dưới 0,1. Nhảy từ 0 lên 0,138 nghĩa là có thứ gì đó trong trang bắt đầu giật — và tôi vừa mới sờ vào một loạt thứ, nên chẳng biết quy cho cái nào.

Mất bốn giả thuyết tôi mới tìm ra. Cả bốn đều bị chính bằng chứng tôi đi thu thập bác bỏ. Bài này ghi lại đủ bốn lần sai, vì phần sai mới là phần dạy được nhiều nhất.

Con số bắt tôi phải đi tìm

Rắc rối lớn nhất không nằm ở con số, mà ở chỗ tôi vừa làm ba việc sát nhau: cắt bớt file CSS của theme, dựng lại critical CSS, và đổi cách nạp ảnh nền. Khi ba thay đổi đứng cạnh nhau trong cùng một tuần, không lần đo nào tách được ai gây ra cái gì.

Kinh nghiệm từ hai lần trước nói rằng đoán mò ở giai đoạn này rất tốn thời gian. Tôi đã từng cắt 80% file CSS của theme rồi ngồi chờ LCP nhúc nhích mà nó không nhúc nhích — chuyện đó tôi kể trong bài Cắt 80% file CSS của theme mà LCP không nhúc nhích. Nên lần này tôi đặt luật cho chính mình: mỗi giả thuyết phải chết bằng một bằng chứng cụ thể, không chết bằng cảm giác.

Bốn lần tôi tưởng đã bắt được thủ phạm

Giả thuyết 1 — hai hệ hộp cookie chồng lên nhau. Tôi grep chuỗi consent-bar trong mã nguồn và ra 10 dòng. Mười dòng nghe như bằng chứng của một mớ hỗn độn. Đọc kỹ từng dòng thì tất cả đều là cky-consent-bar — tên lớp của chính plugin CookieYes, xuất hiện ở 10 chỗ khác nhau trong CSS của nó. Không có hệ thứ hai nào. Đếm số dòng grep không phải là đếm số thành phần.

Giả thuyết 2 — hộp cookie đẩy nội dung tụt xuống. Nghe rất hợp lý: banner hiện lên, nội dung bị đùn đi, bố cục xô. Mở CSS của plugin ra xem thì .cky-consent-container khai position: fixed. Phần tử fixed bị lấy ra khỏi luồng bố cục, nó nằm đè lên trang chứ không chiếm chỗ của ai. Giả thuyết này chết mà không cần đo lần nào — chỉ cần đọc đúng một dòng khai báo.

Giả thuyết 3 — ảnh thiếu khai chiều rộng và chiều cao. Đây là nguyên nhân CLS kinh điển: trình duyệt không biết ảnh cao bao nhiêu nên chừa chỗ sai, ảnh về là mọi thứ tụt. Tôi quét trang chủ: 77 trên 79 ảnh có đủ cả hai thuộc tính. Hai ảnh thiếu là icon của plugin, và chúng cũng đang position: fixed. Không đủ để tạo ra 0,138.

Giả thuyết 4 — swiper và font-awesome nạp trùng hai lần. Cái này tôi tự tin nhất, vì nhìn HTML thấy rõ hai thẻ giống nhau. Nhưng bản thứ hai nằm gọn trong <noscript>. Đó là dự phòng cố ý của mu-plugins/mna-css-diet.php: kỹ thuật nạp CSS bằng media="print" onload chỉ chạy khi có JavaScript, nên phải để sẵn một bản trong <noscript> cho khách tắt JS. Trùng lặp ở đây là tính năng, không phải lỗi. Ai đó — chính là tôi, ba tuần trước — đã cố ý đặt nó ở đấy.

Bảng năm giả thuyết về nguồn CLS trang chủ marketing365.vn, bốn cái bị bác, cái thứ năm đúng; kèm bảng số trước và sau
Năm giả thuyết theo đúng thứ tự tôi nghĩ ra, và số đo trước — sau. Phần CLS và điểm lấy từ PageSpeed Insights ngày 22/07/2026; phần byte font và số thẻ preload đo trực tiếp trên máy chủ ngày 14/08/2026. Hai cách đo khác nhau, tôi để tách bạch chứ không trộn.

Cái duy nhất còn lại sau khi gạch hết

Gạch hết bốn cái thì còn đúng một thứ tôi vừa đụng vào mà chưa xét: font. Tuần đó tôi vừa gỡ font khỏi fonts.googleapis.com về tự host trong thư mục theme, và giữ nguyên font-display: swap.

swap nghĩa là: vẽ chữ ngay bằng font có sẵn trong máy khách, khi file woff2 tải xong thì thay vào. Nghe thì hay — chữ hiện sớm, không có khoảng trắng chờ. Vấn đề là hai mặt chữ hiếm khi rộng bằng nhau. Font hệ thống hẹp hơn một chút, font thật rộng hơn một chút, thế là ở khoảnh khắc thay chỗ, mỗi dòng chữ dài ra hoặc ngắn lại, số dòng của một đoạn có thể đổi, và mọi khối nằm bên dưới bị đẩy đi. Đúng định nghĩa của xô lệch bố cục.

Cách chữa không phải bỏ swap, mà là làm cho font tạm có kích thước gần bằng font thật. CSS cho phép khai một @font-face trỏ vào font hệ thống rồi ép lại số đo của nó:

@font-face {
  font-family: 'BVP Fallback';
  src: local('Arial'), local('Helvetica'), local('Roboto');
  size-adjust: 100.2%;
  ascent-override: 99.8%;
  descent-override: 26.4%;
  line-gap-override: 0%;
}

Bốn con số đó không phải bịa. Tôi lấy chúng bằng fontTools, đọc xAvgCharWidth và bảng hhea của chính hai font web đang dùng, rồi so với Liberation Sans (đứng thay Arial) và Liberation Serif (đứng thay Georgia). Bản cho Playfair Display ra bộ số khác: 102,0% / 106,1% / 24,6%. Sau đó chèn tên font dự phòng vào 16 khai báo font-family nằm rải trong style.csshome-magazine.css — thiếu một chỗ là chỗ đó vẫn giật.

Tự host font: mấy chỗ dễ làm hỏng

Việc kéo font về máy mình nghe đơn giản, nhưng có vài chỗ sai là mất trắng công sức:

  1. Thẻ preload font bắt buộc có crossorigin. Font luôn được tải ở chế độ CORS. Thiếu thuộc tính này, thẻ preload không khớp với lần tải thật, và trình duyệt tải file lần thứ hai — chậm hơn lúc chưa preload.
  2. Giữ nguyên unicode-range khi tách bộ ký tự. Đây là thứ cho trình duyệt bỏ qua file nó không cần. Bỏ dòng này đi thì máy khách nào cũng tải cả ba bộ, dù chỉ hiển thị tiếng Việt.
  3. Chỉ preload thứ chắc chắn dùng ở màn hình đầu. Tôi preload Be Vietnam Pro 400 ở mọi trang, còn Playfair Display 700 chỉ ở trang chủ và trang bài viết — hai nơi tiêu đề lớn thật sự nằm trên đầu màn hình. Preload thừa thì tranh băng thông với thứ cần hơn.

Kết quả trên máy chủ hôm nay: 15 file woff2 tự host, tổng 279.356 B cho Be Vietnam Pro 400/600/700 và Playfair Display 600/700, mỗi font ba bộ vietnamese + latin + latin-ext. Bộ cyrillic tôi bỏ hẳn — trang này không có chữ Nga. Trang chủ in ra 5 thẻ preload font, tất cả đều có crossorigin.

Nghiệm thu, kể cả con số đi ngược

Đo lại lúc 22:30 giờ VN cùng ngày, vẫn bằng PageSpeed Insights để so được với lần trước: CLS máy tính bàn 0,138 xuống 0,023, CLS điện thoại 0,036 xuống 0,018. Điểm máy tính bàn 89 lên 93 (FCP 0,8 xuống 0,5 giây, LCP 1,2 xuống 1,0 giây). Điểm điện thoại 71 lên 76 (FCP 3,0 xuống 2,0 giây, LCP 5,7 xuống 5,3 giây).

Đến đây thì giả thuyết thứ năm được chứng minh: font swap đúng là nguồn CLS, và font dự phòng khớp số đo đã triệt nó.

Nhưng có một con số đi ngược, và tôi không giấu nó: TBT máy tính bàn từ 30 ms lên 150 ms. Tôi chưa biết đó là hậu quả thật của thay đổi hay chỉ là nhiễu của một lần chạy — PageSpeed chạy trên máy chia sẻ, một lần đo đơn lẻ dao động khá rộng. Ghi nhận là ghi nhận, chưa kết luận. Cũng cần nói thẳng: điểm điện thoại 76 vẫn chưa xanh, và việc điều tra tiếp dẫn tôi sang một chuyện hoàn toàn khác — trang chủ không được cache như tôi tưởng.

Thứ tôi mang đi khỏi vụ này

Bốn giả thuyết sai để lại bốn bài học rõ hơn cả cái đúng. Đếm số dòng grep không phải là bằng chứng — mười dòng có thể là một thứ lặp lại mười lần. Một dòng khai báo CSS đôi khi giết được cả giả thuyết mà không cần đo: position: fixed thì không thể xô bố cục, hết chuyện. Và trùng lặp trong mã nguồn chưa chắc là lỗi; trước khi gỡ, hãy tìm xem ai đã cố ý đặt nó ở đó và vì lý do gì.

Bài học thứ tư thì đắt hơn: khi nhiều thay đổi đứng cạnh nhau, cái đúng thường là cái tôi không nghĩ tới, đơn giản vì tôi đã coi nó là “đã xong”. Font là việc tôi vừa làm và tự chấm điểm là thành công, nên nó nằm ngoài danh sách nghi phạm suốt bốn vòng. Chuyện y hệt lặp lại ở vụ tôi tin một cái nhãn công cụ mà đi sai mất 11 ngày — kể trong bài 11 ngày sửa nhầm chỗ vì tin một cái nhãn sai.

Còn một chỗ chưa dọn, và tôi ghi ra đây thay vì im lặng. Đo lại hôm nay, chữ “googleapis” vẫn còn đúng một lần trên trang chủ — nằm trong thẻ preconnect. Preconnect là lời dặn trình duyệt mở sẵn kết nối tới một máy chủ sắp phải gọi tới. Từ ngày tự host font, không còn request nào đi tới đó nữa, nên lời dặn ấy chỉ tốn một lần bắt tay mạng cho hư không. Tôi để nguyên và ghi lại chứ chưa gỡ: gỡ là một thay đổi riêng, cần một lần đo riêng để biết nó có thật sự đổi gì không. Sửa mà không đo thì lại quay về đúng chỗ tôi vừa đi ra.

Có thể bạn thích

Để lại bình luận