Giới thiệu: Font web — kẻ gây layout shift mà ít ai để ý
Khi nói đến tối ưu hiệu suất web, hầu hết mọi người nghĩ ngay đến hình ảnh, JavaScript bundle hay script bên thứ ba. Nhưng có một "thủ phạm thầm lặng" mà nhiều developer bỏ qua: font web.
Theo dữ liệu từ HTTP Archive năm 2026, trung bình mỗi trang web tải khoảng 5-8 file font, chiếm 100-300KB dung lượng — và quan trọng hơn, chúng ảnh hưởng trực tiếp đến cả ba chỉ số Core Web Vitals: LCP, CLS và INP. Nói thật, con số này làm mình khá bất ngờ khi lần đầu nhìn thấy.
Mình từng audit một trang thương mại điện tử sử dụng 4 font family với tổng cộng 12 file font. Kết quả? CLS tăng vọt lên 0.25 (gấp 2.5 lần ngưỡng cho phép) chỉ vì font swap, và LCP chậm thêm 800ms do các file font chặn render. Sau khi áp dụng các kỹ thuật trong bài này, CLS giảm xuống 0.02 và LCP cải thiện 600ms. Sự khác biệt rõ ràng đến mức cả team product cũng nhận ra.
Bài viết này sẽ hướng dẫn bạn từ A đến Z cách tối ưu font web trong năm 2026: từ việc chọn đúng giá trị font-display, subsetting, self-hosting, cho đến kỹ thuật nâng cao như fallback font metrics matching — tất cả đều kèm code thực tế có thể áp dụng ngay.
1. Font web ảnh hưởng đến Core Web Vitals như thế nào?
1.1 Tác động lên Largest Contentful Paint (LCP)
Khi phần tử LCP là text (tiêu đề, đoạn văn lớn), font web ảnh hưởng trực tiếp đến thời gian hiển thị. Trình duyệt phải hoàn thành các bước sau trước khi render text:
- Tải xuống file CSS chứa
@font-face(hoặc stylesheet từ Google Fonts) - Parse CSS để phát hiện các font cần tải
- Tải xuống file font (.woff2)
- Decode và render text với font đã tải
Mỗi bước đều thêm latency. Nếu font được load từ bên thứ ba (như Google Fonts), còn phải thêm DNS lookup và TLS handshake — dễ dàng thêm 200-500ms vào LCP.
Nhớ rằng, ngưỡng LCP tốt theo Google là dưới 2.5 giây, nhưng các trang web top đầu năm 2026 thường đạt khoảng 1.2-1.8 giây. Nên 200-500ms chậm thêm là khá đáng kể.
1.2 Tác động lên Cumulative Layout Shift (CLS)
Đây là nơi font web gây ra nhiều vấn đề nhất. Khi font web tải xong và thay thế fallback font, text có thể "nhảy" do sự khác biệt về kích thước giữa hai font. Hiện tượng này tạo ra layout shift — và Google đo lường qua chỉ số CLS (ngưỡng tốt: dưới 0.1).
Có hai hiện tượng chính khi font web chưa tải xong:
- FOIT (Flash of Invisible Text): Text hoàn toàn vô hình cho đến khi font tải xong — trải nghiệm người dùng cực kỳ tệ
- FOUT (Flash of Unstyled Text): Text hiển thị bằng fallback font trước, sau đó swap sang font web — gây layout shift nếu metrics hai font khác nhau
Cá nhân mình thấy FOUT dễ chấp nhận hơn FOIT rất nhiều, nhưng cả hai đều cần được xử lý.
1.3 Tác động lên First Contentful Paint (FCP)
Font web cũng ảnh hưởng gián tiếp đến FCP. File CSS chứa @font-face là render-blocking resource — trình duyệt phải parse xong CSS trước khi render bất kỳ nội dung nào. Nếu CSS font được load từ external stylesheet (đặc biệt từ domain khác), FCP sẽ bị trì hoãn đáng kể.
2. Chiến lược font-display: Chọn đúng giá trị cho từng trường hợp
2.1 Hiểu rõ từng giá trị font-display
Thuộc tính font-display trong @font-face kiểm soát cách trình duyệt xử lý font trong khi chờ tải. Nào, cùng xem qua từng giá trị:
font-display: swap— Hiển thị fallback font ngay lập tức, swap sang web font khi tải xong. Block period cực ngắn (~100ms), swap period vô hạn. Ưu điểm: Text luôn hiển thị, LCP tốt. Nhược điểm: Có thể gây CLS khi swap font.font-display: optional— Nếu font chưa sẵn sàng trong ~100ms, dùng fallback font và không swap. Font được tải ngầm cho lần truy cập sau. Ưu điểm: CLS bằng 0. Nhược điểm: Lần truy cập đầu có thể không thấy web font.font-display: fallback— Block period ~100ms, swap period ~3 giây. Cân bằng giữa swap và optional. Nói thật thì mình ít khi dùng giá trị này trong thực tế.font-display: block— Block period dài (~3 giây), text vô hình trong thời gian chờ. Nên tránh vì gây FOIT nghiêm trọng.
2.2 Khuyến nghị cho từng loại trang
Không có giá trị font-display nào "one-size-fits-all" cả. Dưới đây là khuyến nghị dựa trên kinh nghiệm thực tế của mình:
- Trang nội dung (blog, tin tức): Dùng
font-display: optional— ưu tiên CLS vì text là nội dung chính - Trang landing page: Dùng
font-display: swapkết hợp preload — cần hiển thị font brand nhưng cũng cần text xuất hiện nhanh - E-commerce: Dùng
font-display: swapcho heading,font-display: optionalcho body text - Web app (SPA): Dùng
font-display: optional— font sẽ được cache sau lần tải đầu
/* Ví dụ: Chiến lược font-display khác nhau cho heading và body */
@font-face {
font-family: 'Montserrat';
src: url('/fonts/montserrat-bold.woff2') format('woff2');
font-weight: 700;
font-display: swap; /* Heading - cần hiển thị font brand */
font-style: normal;
}
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-regular.woff2') format('woff2');
font-weight: 400;
font-display: optional; /* Body text - ưu tiên zero CLS */
font-style: normal;
}
3. Preload font — Tải font sớm hơn để giảm LCP
3.1 Cách preload hoạt động
Thẻ <link rel="preload"> báo cho trình duyệt bắt đầu tải font ngay lập tức, thay vì chờ đến khi parse xong CSS mới phát hiện font cần tải. Đơn giản vậy thôi, nhưng hiệu quả có thể cắt giảm 200-500ms khỏi thời gian tải font.
<!-- Preload font quan trọng nhất -->
<link
rel="preload"
href="/fonts/inter-var-latin.woff2"
as="font"
type="font/woff2"
crossorigin
>
<!-- LƯU Ý: crossorigin là BẮT BUỘC cho font,
kể cả khi self-hosting trên cùng domain -->
3.2 Quy tắc vàng khi preload font
Preload là con dao hai lưỡi. Nếu preload quá nhiều font, bạn sẽ chiếm dụng băng thông từ các resource quan trọng khác (như LCP image). Đây là những quy tắc mình luôn tuân thủ:
- Chỉ preload tối đa 1-2 file font — những font dùng cho above-the-fold content
- Chỉ preload font format WOFF2 — format nhỏ nhất, được mọi trình duyệt hiện đại hỗ trợ
- Luôn thêm thuộc tính
crossorigin— font được fetch dưới dạng CORS request, thiếu attribute này sẽ khiến font bị tải 2 lần (mình đã mắc lỗi này và mất gần nửa ngày debug) - Không preload font có
font-display: optional— vì nếu font không tải kịp trong 100ms, nó cũng không được sử dụng, gây lãng phí băng thông
3.3 Preconnect cho font từ bên thứ ba
Nếu bạn vẫn sử dụng Google Fonts (hoặc font CDN khác), hãy dùng preconnect để thiết lập kết nối sớm:
<!-- Preconnect đến Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Sau đó load font stylesheet -->
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap"
rel="stylesheet"
>
Tuy nhiên, giải pháp tốt nhất vẫn là self-hosting — sẽ nói chi tiết ở phần tiếp theo.
4. WOFF2: Format duy nhất bạn cần trong 2026
4.1 Tại sao chỉ cần WOFF2?
Trong năm 2026, WOFF2 đã được hỗ trợ bởi hơn 97% trình duyệt toàn cầu. WOFF2 sử dụng Brotli compression, nhỏ hơn 30% so với WOFF và nhỏ hơn 60% so với TTF.
Không có lý do gì để ship nhiều format font nữa. Thật sự là vậy.
Một ví dụ cụ thể — font Montserrat Regular ở các format khác nhau:
- TTF: 225 KB
- WOFF: 94 KB
- WOFF2: 83 KB
Với mỗi font weight bạn thêm vào, sự chênh lệch càng tích lũy. 4 font weights × 60% giảm = tiết kiệm hàng trăm KB. Đó là bandwidth thật sự tiết kiệm được cho người dùng 4G.
4.2 Chuyển đổi font sang WOFF2
# Cài đặt woff2 compression tool
# macOS
brew install woff2
# Ubuntu/Debian
sudo apt-get install woff2
# Chuyển đổi TTF sang WOFF2
woff2_compress ./fonts/MyFont-Regular.ttf
# Output: ./fonts/MyFont-Regular.woff2
# Chuyển đổi hàng loạt
for f in ./fonts/*.ttf; do
woff2_compress "$f"
done
4.3 Khai báo @font-face chỉ với WOFF2
/* Đơn giản, sạch sẽ, hiệu quả */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* KHÔNG CẦN viết fallback format như thế này nữa:
src: url(font.woff2) format('woff2'),
url(font.woff) format('woff'),
url(font.ttf) format('truetype');
*/
5. Font Subsetting — Cắt bỏ ký tự không cần thiết
5.1 Vấn đề: Font chứa quá nhiều glyphs
Hầu hết font phổ biến chứa hàng nghìn glyphs cho nhiều ngôn ngữ: Latin, Cyrillic, Greek, tiếng Ả Rập, tiếng Việt, ký hiệu toán học... Nếu trang web của bạn chỉ sử dụng tiếng Việt và tiếng Anh, bạn đang tải thêm hàng trăm KB ký tự không bao giờ dùng đến. Lãng phí đúng không?
5.2 Subsetting với pyftsubset
pyftsubset từ thư viện fonttools là công cụ subsetting mình hay dùng nhất — miễn phí và rất mạnh:
# Cài đặt fonttools
pip install fonttools brotli
# Subset font chỉ giữ Latin + Vietnamese
pyftsubset MyFont-Regular.ttf \
--output-file=MyFont-Regular-vi.woff2 \
--flavor=woff2 \
--layout-features='*' \
--unicodes="U+0000-00FF,U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303,U+0309,U+0323,U+1EA0-1EF9,U+2000-206F,U+2074,U+20AB,U+20AC,U+2122,U+2191,U+2193,U+2212,U+FEFF,U+FFFD"
# Giải thích các unicode range:
# U+0000-00FF: Basic Latin + Latin-1 Supplement
# U+0102-0103: Ă ă (tiếng Việt)
# U+0110-0111: Đ đ (tiếng Việt)
# U+0128-0129: Ĩ ĩ
# U+0168-0169: Ũ ũ
# U+01A0-01A1: Ơ ơ
# U+01AF-01B0: Ư ư
# U+0300-0301,U+0303,U+0309,U+0323: Dấu thanh tiếng Việt
# U+1EA0-1EF9: Vietnamese precomposed characters
# U+20AB: Ký hiệu đồng Việt Nam (₫)
5.3 Sử dụng unicode-range trong CSS
Nếu bạn cần hỗ trợ nhiều ngôn ngữ, hãy tách font thành các subset và dùng unicode-range để trình duyệt chỉ tải đúng subset cần thiết:
/* Subset cho tiếng Việt */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-vietnamese.woff2') format('woff2');
font-weight: 400;
font-display: swap;
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129,
U+0168-0169, U+01A0-01A1, U+01AF-01B0,
U+0300-0301, U+0303, U+0309, U+0323,
U+1EA0-1EF9, U+20AB;
}
/* Subset cho Latin cơ bản */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-latin.woff2') format('woff2');
font-weight: 400;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153,
U+02BB-02BC, U+02C6, U+02DA, U+02DC,
U+2000-206F, U+2074, U+20AC, U+2122,
U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Trình duyệt sẽ chỉ tải file font nào chứa
ký tự thực sự xuất hiện trên trang */
5.4 Kết quả thực tế
Mình đã thực hiện subsetting cho font Inter trên một dự án gần đây và kết quả khá ấn tượng:
- Trước: Inter Regular (full) — 98 KB (WOFF2)
- Sau: Inter Regular (Latin + Vietnamese) — 32 KB (WOFF2)
- Giảm 67% dung lượng — nhân với 4 font weights = tiết kiệm ~260 KB
260 KB nghe có vẻ nhỏ, nhưng trên mạng 3G/4G chậm, đó là sự khác biệt mà người dùng cảm nhận được.
6. Self-Hosting Font: Tại sao và cách thực hiện
6.1 Tại sao nên self-host thay vì dùng Google Fonts?
Google Fonts rất tiện lợi — không ai phủ nhận điều đó. Nhưng có nhiều nhược điểm về hiệu suất mà ít người để ý:
- Extra connections: Trình duyệt phải thiết lập kết nối đến
fonts.googleapis.com(cho CSS) VÀfonts.gstatic.com(cho file font) — 2 DNS lookups, 2 TLS handshakes - Cache không được chia sẻ nữa: Từ khi Chrome thay đổi cache partitioning (năm 2020), font Google Fonts đã cache trên site A sẽ KHÔNG được dùng lại khi truy cập site B — lợi thế cache chia sẻ đã chết
- Không kiểm soát caching: Google Fonts thiết lập cache header ngắn hạn, bạn không thể tùy chỉnh theo ý muốn
- Vấn đề GDPR/quyền riêng tư: Mỗi request đến Google Fonts gửi IP người dùng — một số quốc gia EU đã phạt vì vi phạm GDPR. Đây là vấn đề pháp lý thật sự cần lưu ý nếu bạn có traffic từ châu Âu.
6.2 Quy trình self-host từ Google Fonts
# Bước 1: Tải font từ Google Fonts
# Sử dụng google-webfonts-helper hoặc tải trực tiếp
# Bước 2: Subset và convert sang WOFF2
pyftsubset Inter-Regular.ttf \
--output-file=inter-regular-vi.woff2 \
--flavor=woff2 \
--layout-features='*' \
--unicodes="U+0000-00FF,U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303,U+0309,U+0323,U+1EA0-1EF9,U+20AB,U+20AC"
# Bước 3: Đặt file vào thư mục static
# /public/fonts/inter-regular-vi.woff2
# /public/fonts/inter-bold-vi.woff2
6.3 Cấu hình server cache headers
# Nginx - Cache font files dài hạn
location ~* \.(woff2)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
add_header Access-Control-Allow-Origin "*";
expires 1y;
}
# Apache - .htaccess
<FilesMatch "\.(woff2)$">
Header set Cache-Control "public, max-age=31536000, immutable"
Header set Access-Control-Allow-Origin "*"
</FilesMatch>
Keyword immutable rất quan trọng — nó báo cho trình duyệt rằng file này sẽ không bao giờ thay đổi, nên không cần revalidate. Kết hợp với filename hashing (ví dụ: inter-regular.abc123.woff2), bạn có chiến lược caching tối ưu nhất có thể.
7. Fallback Font Metrics Matching — Kỹ thuật triệt tiêu CLS từ font
7.1 Vấn đề cốt lõi
Ngay cả khi dùng font-display: swap, CLS vẫn xảy ra nếu fallback font và web font có metrics khác nhau. Ví dụ: font Inter có chiều cao dòng, ascender và descender khác với font hệ thống Arial — khi swap xảy ra, text "nhảy" vì chiều cao thay đổi.
Đây là phần mà mình thấy nhiều developer bỏ qua nhất, dù hiệu quả lại rất đáng kể.
7.2 CSS @font-face descriptors mới
CSS hiện đại cung cấp bốn thuộc tính trong @font-face để điều chỉnh metrics của fallback font:
size-adjust: Điều chỉnh kích thước tổng thể của fallback font (dựa trên tỷ lệ chiều rộng ký tự trung bình)ascent-override: Ghi đè chiều cao phần trên của ký tự (phần trên baseline)descent-override: Ghi đè chiều cao phần dưới của ký tự (phần dưới baseline)line-gap-override: Ghi đè khoảng cách giữa các dòng
7.3 Ví dụ thực tế: Matching Inter với Arial
/* Bước 1: Định nghĩa fallback font với metrics đã điều chỉnh */
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107.64%;
ascent-override: 90.49%;
descent-override: 22.48%;
line-gap-override: 0%;
}
/* Bước 2: Định nghĩa web font chính */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-regular.woff2') format('woff2');
font-weight: 400;
font-display: swap;
}
/* Bước 3: Sử dụng trong font stack */
body {
font-family: 'Inter', 'Inter Fallback', sans-serif;
}
/* Khi trang tải:
1. Text hiển thị ngay bằng 'Inter Fallback' (Arial đã điều chỉnh)
2. Khi Inter tải xong, swap diễn ra
3. Vì metrics đã match, KHÔNG có layout shift */
7.4 Ví dụ cho font tiếng Việt: Matching Roboto với system font
/* Fallback cho Roboto - Linux/Android */
@font-face {
font-family: 'Roboto Fallback';
src: local('Arial'), local('Helvetica Neue');
size-adjust: 100.3%;
ascent-override: 92.77%;
descent-override: 24.41%;
line-gap-override: 0%;
}
/* Roboto chính */
@font-face {
font-family: 'Roboto';
src: url('/fonts/roboto-regular-vi.woff2') format('woff2');
font-weight: 400;
font-display: swap;
unicode-range: U+0000-00FF, U+0102-0103, U+0110-0111,
U+0128-0129, U+0168-0169, U+01A0-01A1,
U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
body {
font-family: 'Roboto', 'Roboto Fallback', system-ui, sans-serif;
}
7.5 Công cụ tính toán metrics tự động
Bạn không cần tự tính toán các giá trị trên bằng tay (và thật lòng là cũng không nên). Có nhiều công cụ hỗ trợ:
- Fallback Font Generator (screenspan.net/fallback) — Giao diện trực quan, kéo thả để điều chỉnh
- Fontaine — Thư viện JavaScript/Vite/Webpack plugin tự động tính metrics
- Capsize (@capsizecss/core) — API programmatic để generate CSS font stack
- Font Face Metrics Adjuster — Công cụ online cho phép so sánh visual trực tiếp
8. Tích hợp với Framework: Next.js, Nuxt và Vite
8.1 Next.js — next/font (Tự động tối ưu)
Từ Next.js 13, next/font tự động thực hiện mọi tối ưu mà chúng ta đã nói ở trên: self-hosting, subsetting, fallback metrics matching, và preload. Nếu bạn dùng Next.js, đây là cách đơn giản nhất:
// app/layout.tsx
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin', 'vietnamese'],
display: 'swap',
// next/font tự động:
// 1. Download và self-host font tại build time
// 2. Tạo fallback font với metrics matching
// 3. Thêm preload cho font file
// 4. Generate @font-face CSS tối ưu
});
export default function RootLayout({ children }) {
return (
<html lang="vi" className={inter.className}>
<body>{children}</body>
</html>
);
}
Thật sự rất tiện — gần như zero-config mà kết quả đã rất tốt rồi.
8.2 Nuxt — @nuxtjs/fontaine
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@nuxtjs/fontaine'
],
// Fontaine tự động scan @font-face trong CSS
// và generate fallback fonts với metrics matching
})
8.3 Vite/Webpack — Fontaine Plugin
// vite.config.js
import { FontaineTransform } from 'fontaine';
export default {
plugins: [
FontaineTransform.vite({
fallbacks: ['Arial', 'Helvetica Neue'],
// Tự động tạo @font-face với metrics override
// cho mọi font được khai báo trong CSS
})
]
}
9. Variable Fonts — Một file, mọi weight
9.1 Ưu điểm của Variable Fonts
Variable fonts là bước tiến lớn cho hiệu suất web. Thay vì tải nhiều file font cho từng weight (Regular, Medium, Bold, Black...), bạn chỉ cần một file duy nhất chứa mọi biến thể.
Ví dụ với font Inter:
- Static fonts: 4 weights × ~32 KB = 128 KB
- Variable font: 1 file = ~95 KB (chứa mọi weight từ 100 đến 900)
- Tiết kiệm ~26% dung lượng VÀ giảm từ 4 HTTP requests xuống 1
Giảm requests đôi khi còn quan trọng hơn giảm dung lượng, đặc biệt trên mạng có latency cao.
9.2 Khai báo Variable Font
/* Variable font - một file cho mọi weight */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var-latin-vi.woff2') format('woff2-variations');
font-weight: 100 900; /* Range thay vì giá trị cố định */
font-style: normal;
font-display: swap;
}
/* Sử dụng bất kỳ weight nào trong range */
h1 { font-weight: 800; }
h2 { font-weight: 650; } /* Giá trị tùy ý, không bị giới hạn! */
p { font-weight: 400; }
.caption { font-weight: 350; }
9.3 Google Fonts API v2 và Variable Fonts
Google Fonts API v2 hỗ trợ đầy đủ variable fonts với cú pháp mới:
<!-- API v2: Request variable font với weight range -->
<link href="https://fonts.googleapis.com/css2?family=Inter:[email protected]&display=swap" rel="stylesheet">
<!-- So sánh với API v1 (static fonts): -->
<!-- Phải list từng weight: family=Inter:400,500,600,700 -->
10. Checklist tối ưu font web toàn diện
Đây là checklist mình dùng cho mọi dự án — bạn có thể bookmark lại để dùng dần:
- Audit font sử dụng: Kiểm tra thực tế dùng bao nhiêu font family và weight — giới hạn tối đa 2 family, 3-4 weights
- Chuyển sang WOFF2: Loại bỏ mọi format cũ (TTF, EOT, WOFF1)
- Subset font: Chỉ giữ unicode range cần thiết (Latin + Vietnamese cho trang tiếng Việt)
- Self-host font: Tránh dependency vào Google Fonts CDN
- Thiết lập font-display:
swapcho heading,optionalcho body text - Preload font critical: Tối đa 1-2 file font dùng cho above-the-fold
- Cấu hình fallback metrics: Dùng Fontaine hoặc Capsize để match metrics
- Cache headers:
Cache-Control: public, max-age=31536000, immutable - Cân nhắc variable fonts: Nếu dùng từ 3 weights trở lên, variable font thường nhỏ hơn
- Kiểm tra CLS: Dùng Chrome DevTools Performance tab và Lighthouse để verify kết quả
11. Đo lường và theo dõi hiệu suất font
11.1 Chrome DevTools
Mở tab Performance, ghi lại quá trình tải trang và tìm kiếm các sự kiện liên quan đến font:
- Layout Shift events — check attribution để xem có phải do font gây ra
- Font download trong Network waterfall — kiểm tra timing và kích thước
- Rendering tab > bật "Layout Shift Regions" để thấy visual highlight khi layout shift xảy ra
11.2 Performance Observer API
// Theo dõi layout shift do font gây ra
const clsObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
console.log('Layout Shift:', entry.value.toFixed(4));
// Kiểm tra sources để xác định nguyên nhân
entry.sources?.forEach(source => {
console.log(' Element:', source.node?.nodeName);
console.log(' Previous rect:', source.previousRect);
console.log(' Current rect:', source.currentRect);
});
}
}
});
clsObserver.observe({ type: 'layout-shift', buffered: true });
// Theo dõi thời gian tải font
const resourceObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.initiatorType === 'css' &&
entry.name.includes('.woff2')) {
console.log(`Font loaded: ${entry.name}`);
console.log(` Duration: ${entry.duration.toFixed(0)}ms`);
console.log(` Transfer size: ${entry.transferSize} bytes`);
}
}
});
resourceObserver.observe({ type: 'resource', buffered: true });
11.3 Lighthouse Audit
Lighthouse cung cấp nhiều audit liên quan đến font mà bạn nên chú ý:
- "Ensure text remains visible during webfont load" — kiểm tra
font-display - "Preload key requests" — phát hiện font nên được preload
- "Avoid enormous network payloads" — cảnh báo file font quá lớn
- "Minimize main-thread work" — bao gồm thời gian parse font CSS
Câu hỏi thường gặp (FAQ)
font-display: swap và font-display: optional khác nhau thế nào?
font-display: swap hiển thị fallback font ngay lập tức và swap sang web font khi tải xong — luôn đảm bảo web font được sử dụng nhưng có thể gây layout shift (CLS). Còn font-display: optional chỉ sử dụng web font nếu nó tải xong trong khoảng 100ms đầu tiên — nếu không, giữ nguyên fallback font và tải web font ngầm cho lần truy cập sau. optional cho CLS bằng 0 nhưng lần truy cập đầu có thể không thấy web font.
Có nên tiếp tục dùng Google Fonts không?
Trong năm 2026, self-hosting là lựa chọn tốt hơn cho hiệu suất. Lợi thế cache chia sẻ của Google Fonts đã không còn từ khi Chrome áp dụng cache partitioning (2020). Self-hosting giúp giảm DNS lookup, cho phép kiểm soát caching, và tránh vấn đề GDPR. Tuy nhiên, nếu bạn đang dùng Next.js hoặc Nuxt, các framework này đã tự động self-host Google Fonts tại build time — nên bạn vẫn dùng API Google Fonts được, chỉ là font sẽ được serve từ domain của bạn.
Font subsetting có ảnh hưởng đến SEO không?
Ngược lại — font subsetting giúp cải thiện SEO. File font nhỏ hơn = tải nhanh hơn = LCP và CLS tốt hơn = Core Web Vitals tốt hơn, mà Core Web Vitals là yếu tố xếp hạng của Google. Chỉ cần đảm bảo subset bao gồm đầy đủ unicode range cho ngôn ngữ bạn sử dụng (đặc biệt với tiếng Việt cần thêm các ký tự có dấu).
Làm sao biết font nào đang gây layout shift trên trang?
Mở Chrome DevTools, vào tab Performance và ghi lại quá trình tải trang. Tìm các sự kiện "Layout Shift" trong timeline — click vào để xem chi tiết nguồn gốc. Bạn cũng có thể bật "Layout Shift Regions" trong tab Rendering để thấy highlight visual. Ngoài ra, DebugBear và SpeedCurve cung cấp phân tích chi tiết font-related CLS trong báo cáo RUM.
Variable font có hỗ trợ tốt cho tiếng Việt không?
Có, và khá tốt nữa. Các variable font phổ biến như Inter, Roboto Flex, Noto Sans đều hỗ trợ đầy đủ bảng ký tự tiếng Việt. Browser support cho variable fonts cũng đã đạt hơn 95% toàn cầu trong năm 2026. Bạn vẫn nên subset variable font để chỉ giữ unicode range cần thiết — giảm đáng kể kích thước file mà vẫn giữ đầy đủ weight range.