Font loading sai gây FOIT (chữ vô hình 3 giây), FOUT (font swap), CLS — toàn bộ là vấn đề SEO + UX. Bài này code next/font + font-display + fallback metrics để fix triệt để.
Vấn đề: 3 cái xấu
| Tên | Triệu chứng | Nguyên nhân |
|---|---|---|
| FOIT | Text vô hình, hiện sau khi font load | font-display: block (default cũ) |
| FOUT | Text hiện với fallback, swap khi font load | font-display: swap |
| CLS từ font | Layout dịch chuyển khi swap | Fallback font có metric khác webfont |
font-display directive
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-vn.woff2') format('woff2');
font-display: optional; /* recommend cho perf */
}
| Value | Block period | Swap period | Behavior |
|---|---|---|---|
| auto | 3s | infinite | FOIT 3s, swap khi font load |
| block | 3s | infinite | FOIT lâu |
| swap | 0 | infinite | FOUT + CLS khi swap |
| fallback | 0.1s | 3s | Trung gian |
| optional | 0.1s | 0 | Tốt nhất — không swap nếu không kịp |
optional: text hiện ngay với fallback. Nếu font tải kịp 100ms → render webfont. Không kịp → giữ fallback toàn page (không swap, không CLS). Lần sau revisit, font đã cache → thấy webfont.
next/font — chuẩn 2026
// app/layout.tsx
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin', 'vietnamese'],
display: 'swap', // hoặc 'optional'
variable: '--font-inter',
})
export default function RootLayout({ children }) {
return (
<html lang="vi" className={inter.variable}>
<body className={inter.className}>{children}</body>
</html>
)
}
next/font tự động:
- Download font ở build time
- Self-host (file vào public folder)
- Tránh DNS + connect tới fonts.googleapis.com
- Compute size-adjust cho fallback metrics
- Inline @font-face trong HTML
Local font
import localFont from 'next/font/local'
const sans = localFont({
src: [
{ path: './fonts/Inter-Regular.woff2', weight: '400', style: 'normal' },
{ path: './fonts/Inter-Bold.woff2', weight: '700', style: 'normal' },
],
variable: '--font-sans',
display: 'swap',
})
size-adjust + ascent-override match metrics
Vấn đề CLS: webfont Inter có x-height khác Arial → text "An Nguyen" với Inter dài hơn Arial. Khi swap, layout shift.
Giải pháp: tạo @font-face fallback adjusted:
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107.4%;
ascent-override: 90.20%;
descent-override: 22.48%;
line-gap-override: 0%;
}
body {
font-family: 'Inter', 'Inter Fallback', sans-serif;
}
Số 107.4%, 90.20% là metrics generate riêng cho Inter. next/font auto compute. Tự gen: npx fontaine hoặc tool online font-style-matcher.
Subset đúng
| Subset | Bao gồm | Size (Inter) |
|---|---|---|
| latin | Tiếng Anh, dấu cơ bản | ~30KB |
| latin-ext | + Đông Âu | ~40KB |
| vietnamese | Tiếng Việt có dấu | ~20KB extra |
| cyrillic | Nga, Bulgaria | ~30KB extra |
| full | Mọi script | ~200KB+ |
const inter = Inter({ subsets: ['latin', 'vietnamese'] })
// Chỉ tải 50KB thay vì 200KB
Preload critical font
<link
rel="preload"
href="/fonts/inter-vn-400.woff2"
as="font"
type="font/woff2"
crossorigin
/>
Browser bắt đầu tải font ngay khi parse HTML, không chờ CSS. next/font auto preload 1-2 weight quan trọng.
Kết luận
Font loading đúng = next/font + display: optional + size-adjust fallback. Tốn 5 phút setup, win lớn cho LCP và CLS. Tham khảo Web Vitals để đo impact, và đo cả Lighthouse + RUM trước/sau khi tối ưu.