Bỏ qua menu, vào nội dung chính

Tải font web: tránh FOIT, FOUT và nhảy bố cục

Font loading tối ưu: tránh FOIT (chữ vô hình), FOUT (font swap), CLS (layout shift). Code next/font, font-display, fallback metrics. Performance tăng rõ.

3 phút đọc

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 để.

Trang MDN về thuộc tính font-display
Trang MDN về thuộc tính font-display (developer.mozilla.org), ảnh chụp ngày 27/09/2026.

Vấn đề: 3 cái xấu

TênTriệu chứngNguyên nhân
FOITText vô hình, hiện sau khi font loadfont-display: block (default cũ)
FOUTText hiện với fallback, swap khi font loadfont-display: swap
CLS từ fontLayout dịch chuyển khi swapFallback font có metric khác webfont

font-display directive

css@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-vn.woff2') format('woff2');
  font-display: optional;  /* recommend cho perf */
}
ValueBlock periodSwap periodBehavior
auto3sinfiniteFOIT 3s, swap khi font load
block3sinfiniteFOIT lâu
swap0infiniteFOUT + CLS khi swap
fallback0.1s3sTrung gian
optional0.1s0Tố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

tsx// 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

tsximport 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:

css@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

SubsetBao gồmSize (Inter)
latinTiếng Anh, dấu cơ bản~30KB
latin-ext+ Đông Âu~40KB
vietnameseTiếng Việt có dấu~20KB extra
cyrillicNga, Bulgaria~30KB extra
fullMọi script~200KB+
typescriptconst inter = Inter({ subsets: ['latin', 'vietnamese'] })
// Chỉ tải 50KB thay vì 200KB

Preload critical font

html<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.

Nhận bài mới

Để lại email để nhận bài mới của series Kiến thức lập trình, hoặc theo dõi bằng RSS.

RSS Kiến thức lập trình

Email chỉ dùng để gửi bài mới, xem Chính sách bảo mật.

Trao đổi về dự án

Cần tư vấn?