Next.js Font Optimization

Overview

Next.js provides built-in font optimization through the next/font module, automatically optimizing font loading, eliminating external network requests, and improving performance and user experience.

Why Font Optimization?

Problems with Traditional Font Loading

  1. Layout Shift (CLS): Fonts loading can cause the page layout to jump
  2. Performance Impact: External font requests increase load time
  3. Privacy Concerns: Using external services like Google Fonts may leak user information
  4. Network Dependency: Reliance on third-party CDN availability

Advantages of Next.js Font Optimization

  • Automatically self-host font files
  • Zero layout shift
  • Automatic font subsetting
  • Preload critical fonts
  • Support for Google Fonts and local fonts

Using Google Fonts

Basic Usage

// app/layout.tsx
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
})

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="zh" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

Multiple Font Weights

import { Roboto } from 'next/font/google'

const roboto = Roboto({
  weight: ['400', '700', '900'],
  subsets: ['latin'],
  display: 'swap',
})

Variable Fonts

import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
})

export default function RootLayout({ children }) {
  return (
    <html lang="zh" className={inter.variable}>
      <body>{children}</body>
    </html>
  )
}

Use in CSS:

/* globals.css */
body {
  font-family: var(--font-inter);
}

Using Local Fonts

Loading Local Font Files

// app/layout.tsx
import localFont from 'next/font/local'

const myFont = localFont({
  src: './fonts/my-font.woff2',
  display: 'swap',
})

export default function RootLayout({ children }) {
  return (
    <html lang="zh" className={myFont.className}>
      <body>{children}</body>
    </html>
  )
}

Multiple File Configuration

const myFont = localFont({
  src: [
    {
      path: './fonts/my-font-regular.woff2',
      weight: '400',
      style: 'normal',
    },
    {
      path: './fonts/my-font-bold.woff2',
      weight: '700',
      style: 'normal',
    },
    {
      path: './fonts/my-font-italic.woff2',
      weight: '400',
      style: 'italic',
    },
  ],
  variable: '--font-my-font',
})

Combining Multiple Fonts

Using Multiple Fonts Together

// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap',
})

const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  variable: '--font-roboto-mono',
  display: 'swap',
})

export default function RootLayout({ children }) {
  return (
    <html lang="zh" className={`${inter.variable} ${robotoMono.variable}`}>
      <body>{children}</body>
    </html>
  )
}

Using with Tailwind CSS

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      fontFamily: {
        sans: ['var(--font-inter)'],
        mono: ['var(--font-roboto-mono)'],
      },
    },
  },
}

Usage:

<h1 className="font-sans">标题文本</h1>
<code className="font-mono">代码文本</code>

Font Configuration Options

Full Configuration Example

import { Inter } from 'next/font/google'

const inter = Inter({
  // 字符子集
  subsets: ['latin', 'latin-ext'],
  
  // 字重
  weight: ['400', '500', '600', '700'],
  
  // 字体样式
  style: ['normal', 'italic'],
  
  // 显示策略
  display: 'swap',
  
  // 预加载
  preload: true,
  
  // 回退字体
  fallback: ['system-ui', 'arial'],
  
  // 调整大小
  adjustFontFallback: true,
  
  // CSS 变量名
  variable: '--font-inter',
})

display Options

  • auto: Browser default behavior
  • block: Briefly hide text, then show the font
  • swap: Show fallback font immediately, switch when loaded (recommended)
  • fallback: Hide text briefly, then show fallback font
  • optional: Hide text briefly, may not switch fonts

Using Fonts on Specific Pages

On a Single Page

// app/blog/page.tsx
import { Merriweather } from 'next/font/google'

const merriweather = Merriweather({
  weight: '400',
  subsets: ['latin'],
})

export default function BlogPage() {
  return (
    <div className={merriweather.className}>
      <h1>博客文章</h1>
      <p>这里使用特殊的字体...</p>
    </div>
  )
}

In Components

// components/Heading.tsx
import { Playfair_Display } from 'next/font/google'

const playfair = Playfair_Display({
  subsets: ['latin'],
  weight: '700',
})

export function Heading({ children }) {
  return (
    <h1 className={playfair.className}>
      {children}
    </h1>
  )
}

Performance Optimization Tips

1. Use Font Subsets

const inter = Inter({
  subsets: ['latin'], // 只加载拉丁字符
  // 对于中文网站
  // subsets: ['latin', 'chinese-simplified']
})

2. Limit Font Weights

// ❌ 不推荐:加载所有字重
const roboto = Roboto({
  weight: ['100', '300', '400', '500', '700', '900'],
})

// ✅ 推荐:只加载需要的字重
const roboto = Roboto({
  weight: ['400', '700'],
})

3. Use Variable Fonts

// 可变字体包含所有字重,但文件大小更小
const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
})

4. Preload Critical Fonts

const inter = Inter({
  subsets: ['latin'],
  preload: true, // 预加载字体
  display: 'swap', // 快速显示内容
})

Chinese Font Optimization

Using Local Chinese Fonts

import localFont from 'next/font/local'

const notoSansSC = localFont({
  src: [
    {
      path: './fonts/NotoSansSC-Regular.woff2',
      weight: '400',
    },
    {
      path: './fonts/NotoSansSC-Bold.woff2',
      weight: '700',
    },
  ],
  variable: '--font-noto-sans-sc',
  display: 'swap',
})

Font Subsetting

Since Chinese font files are large, it is recommended to use tools for subsetting:

# 使用 fonttools 进行子集化
pip install fonttools brotli

# 提取常用汉字
pyftsubset font.ttf \
  --output-file=font-subset.woff2 \
  --flavor=woff2 \
  --text-file=common-chars.txt

Debugging and Testing

Checking Font Loading

In browser developer tools:

  1. Network tab: Check font file loading
  2. Performance tab: Check CLS metrics
  3. Lighthouse: Run performance audits

Verifying Font Application

// 添加 data 属性用于调试
<div className={inter.className} data-font="inter">
  内容
</div>

Best Practices

1. Global Font Configuration

// app/layout.tsx
import { Inter, Noto_Sans_SC } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap',
})

const notoSansSC = Noto_Sans_SC({
  subsets: ['chinese-simplified'],
  weight: ['400', '500', '700'],
  variable: '--font-noto-sans-sc',
  display: 'swap',
})

export default function RootLayout({ children }) {
  return (
    <html 
      lang="zh" 
      className={`${inter.variable} ${notoSansSC.variable}`}
    >
      <body>{children}</body>
    </html>
  )
}

2. CSS Configuration

/* app/globals.css */
:root {
  --font-inter: var(--font-inter);
  --font-noto-sans-sc: var(--font-noto-sans-sc);
}

body {
  font-family: var(--font-noto-sans-sc), var(--font-inter), system-ui, sans-serif;
}

code {
  font-family: 'Courier New', monospace;
}

3. Conditional Loading

// 根据语言加载不同字体
import { Inter, Noto_Sans_SC } from 'next/font/google'

const inter = Inter({ subsets: ['latin'] })
const notoSansSC = Noto_Sans_SC({ 
  subsets: ['chinese-simplified'],
  weight: ['400', '700']
})

export default function RootLayout({ 
  children,
  params: { lang }
}) {
  const font = lang === 'zh' ? notoSansSC : inter
  
  return (
    <html lang={lang} className={font.className}>
      <body>{children}</body>
    </html>
  )
}

Practical Example

Complete Font Configuration

// app/fonts.ts
import { Inter, Roboto_Mono, Playfair_Display } from 'next/font/google'
import localFont from 'next/font/local'

// 正文字体
export const inter = Inter({
  subsets: ['latin'],
  variable: '--font-inter',
  display: 'swap',
})

// 代码字体
export const robotoMono = Roboto_Mono({
  subsets: ['latin'],
  variable: '--font-roboto-mono',
  display: 'swap',
})

// 标题字体
export const playfair = Playfair_Display({
  subsets: ['latin'],
  variable: '--font-playfair',
  display: 'swap',
})

// 自定义中文字体
export const customChinese = localFont({
  src: './fonts/custom-chinese.woff2',
  variable: '--font-custom-chinese',
  display: 'swap',
})
// app/layout.tsx
import { inter, robotoMono, playfair, customChinese } from './fonts'

export default function RootLayout({ children }) {
  return (
    <html 
      lang="zh"
      className={`
        ${inter.variable}
        ${robotoMono.variable}
        ${playfair.variable}
        ${customChinese.variable}
      `}
    >
      <body>{children}</body>
    </html>
  )
}
/* app/globals.css */
body {
  font-family: var(--font-custom-chinese), var(--font-inter), sans-serif;
}

h1, h2, h3 {
  font-family: var(--font-playfair), serif;
}

code, pre {
  font-family: var(--font-roboto-mono), monospace;
}

Previous Chapter: Image Optimization | Next Chapter: Metadata