Web Performance · Part 10 — Fonts
Web fonts can slow LCP and cause CLS. Choose WOFF2, a deliberate font-display policy, selective preload, self-hosting, subsetting, and metric-compatible fallbacks.
Web font là con dao hai lưỡi: chúng làm trang đẹp và đúng thương hiệu, nhưng nếu xử lý sai sẽ gây text vô hình (FOIT) làm chậm LCP, hoặc text nhảy (FOUT) gây CLS. Phần này gom chiến lược nạp font để có cả tốc độ lẫn ổn định.
1. Vấn đề: FOIT và FOUT
Khi trang dùng font tùy chỉnh chưa tải xong, trình duyệt phải chọn:
- FOIT (Flash of Invisible Text): ẩn text tới khi font về → người dùng thấy khoảng trống → chậm LCP/FCP nếu text là nội dung chính.
- FOUT (Flash of Unstyled Text): hiện ngay bằng font hệ thống, đổi sang font tùy chỉnh khi về → text “nhảy” kích thước → CLS.
Mục tiêu: hiện text sớm (tránh FOIT) mà hạn chế layout shift. Không có một combo đúng cho mọi site: chọn font-display, preload và fallback metrics theo mức độ quan trọng của thương hiệu rồi đo field.
2. Format: chỉ cần WOFF2
woff2 nén tốt nhất và được hỗ trợ rộng khắp. Bỏ các format cũ (ttf, eot, woff) trừ khi phải hỗ trợ trình duyệt rất cũ — chúng chỉ làm nặng thêm.
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2'); /* woff2 alone is enough */
font-weight: 100 900; /* variable font: one file for every weight */
font-display: swap;
}
Variable font gói một dải weight/width vào một file. Nó có thể nhỏ hơn nhiều file tĩnh khi bạn dùng nhiều biến thể, nhưng không tự động thắng nếu trang chỉ dùng một weight; hãy subset và so tổng byte thực tế.
3. font-display: kiểm soát hành vi swap
font-display quyết định trình duyệt hành xử ra sao khi font đang tải:
| Giá trị | Hành vi | Đánh đổi |
|---|---|---|
swap | hiện fallback ngay, đổi khi font về | nhanh hiện text, nhưng có thể FOUT/CLS |
optional | dùng font nếu về rất nhanh, không thì giữ fallback | tránh late font swap, font có thể không hiện lần đầu |
fallback | thỏa hiệp giữa swap và optional | cân bằng |
block | ẩn text ngắn rồi mới hiện | gần như FOIT, tránh dùng cho nội dung |
Chọn theo ưu tiên:
optionalcho body text khi tốc độ/ổn định quan trọng hơn việc font thương hiệu phải xuất hiện ngay; late font không swap trong lượt đó.swapkhi nội dung phải hiện ngay và bạn muốn cuối cùng dùng web font; ghép fallback khớp metric để giảm shift.blockchỉ hợp trường hợp đặc biệt (ví dụ glyph/icon không có fallback an toàn), và icon SVG thường tốt hơn icon font.
4. Preload font critical
Font khai trong CSS bị phát hiện muộn (trình duyệt phải tải + parse CSS mới biết cần font). preload font dùng cho above-the-fold để nó tải sớm song song:
<!-- preload critical font: crossorigin REQUIRED for fonts (even self-hosted) -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin />
Lưu ý hai điều: chỉ preload font thật sự dùng ngay (thường 1–2 file; preload bỏ qua unicode-range và có thể tải file không cần), và khai crossorigin để request preload khớp chế độ CORS của CSS font fetch — kể cả self-host. Nếu không khớp, preload không được tái sử dụng và trình duyệt có thể phải request lại.
5. size-adjust: giảm CLS khi swap
Ý tưởng: tạo một @font-face fallback từ font hệ thống, tinh chỉnh metric để nó chiếm gần cùng không gian như font tùy chỉnh. size-adjust, ascent-override, descent-override và line-gap-override giảm nguy cơ đổi dòng/chiều cao khi swap, nhưng không bảo đảm tuyệt đối vì hình dạng glyph và rasterization vẫn khác.
/* Illustrative fallback metrics intended to reduce layout movement. */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: 'Inter', 'Inter Fallback', sans-serif; /* fallback sits in the middle */
}
Các con số trên chỉ minh họa; phải tính từ đúng cặp font bằng công cụ như
fontainehoặc cơ chế fallback của framework, rồi xác nhận bằng trace/CLS. Đừng copy tỷ lệ của Inter sang font khác.
6. Cân nhắc self-host thay vì font CDN
Self-host thường nhanh hơn khi asset đi qua cùng CDN/origin với trang:
- Bớt kết nối tới origin CSS/font khác.
- Kiểm soát caching và preload tốt hơn.
- Riêng tư hơn (không gọi sang Google).
Nếu vẫn dùng CDN font, tối thiểu hãy preconnect sớm (Phần 4):
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
7. Subsetting: chỉ nạp ký tự cần
Font đầy đủ chứa hàng nghìn glyph cho nhiều ngôn ngữ. Nếu trang chỉ dùng Latin + tiếng Việt, subset font để bỏ glyph thừa, giảm dung lượng đáng kể. unicode-range chỉ mô tả glyph nào có trong từng file; nó không tự cắt file font:
- Dùng
unicode-rangeđể trình duyệt chỉ tải subset khi trang thật sự dùng ký tự đó:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-latin-vietnamese.woff2') format('woff2');
/* Broad Latin + combining marks + Vietnamese precomposed characters. */
unicode-range: U+0000-024F, U+0300-036F, U+1E00-1EFF, U+20AB;
}
- Subset lúc build bằng
glyphhanger/subset-font(hoặc pipeline font của framework), rồi để tool sinh range chính xác theo glyph thực tế.
Với tiếng Việt, đảm bảo subset bao phủ đủ các nguyên âm có dấu và “đ/Đ” — thiếu sẽ khiến một số chữ rớt về font hệ thống (lệch hình).
8. Checklist fonts
- Chỉ dùng woff2 (cân nhắc variable font)?
- Đã chọn
font-display: optional/swaptheo ưu tiên nội dung và thương hiệu? - Preload 1–2 font critical với
crossorigin? - Có fallback khớp metric (
size-adjust) để giảm shift khi swap? - Self-host hoặc ít nhất preconnect CDN font?
- Subset theo ngôn ngữ thật sự dùng (đủ tiếng Việt)?
Tóm tắt
- Font sai gây FOIT (chậm LCP) hoặc FOUT (CLS); mục tiêu: hiện sớm mà không nhảy.
- Dùng WOFF2 (đo variable font so với các static subset); chọn
optionalhoặcswapcó chủ đích. - Preload font critical (kèm
crossorigin), nhưng đừng preload quá nhiều. size-adjustvà metric override giúp fallback khớp hơn để giảm CLS lúc swap; luôn đo lại.- Self-host + subset theo ngôn ngữ (đủ tiếng Việt) để giảm byte và lượt kết nối.
Phần tiếp theo: Network & caching — HTTP cache, CDN, nén, HTTP/2-3 và service worker.