Bỏ qua đến nội dung chính
font loadingperformanceCLStypographyfrontend

Font Loading: Tránh FOIT, FOUT, Layout Shift

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

Xuất bản 7 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 để.

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

@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

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

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

Zalo