Next.js

字体优化

深入理解 Next.js 字体优化机制,包括 Google Fonts、本地字体、CSS 变量、Tailwind CSS 集成和预加载

概述

next/font 模块自动优化你的字体并移除外部网络请求,以提高隐私和性能。

主要特性:

  • 内置自托管:自动托管任何字体文件
  • 零布局偏移:优化加载 Web 字体,无布局偏移
  • 隐私保护:无需向 Google 发送请求
  • 性能优化:字体在构建时下载并与静态资源一起托管

基本使用

next/font/googlenext/font/local 导入字体,调用函数并设置 className

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

const geist = Geist({
  subsets: ['latin'],
})

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={geist.className}>
      <body>{children}</body>
    </html>
  )
}

字体作用域限定在使用它们的组件中。要将字体应用于整个应用程序,将其添加到根布局。

Google Fonts

可以自动自托管任何 Google 字体。字体作为静态资源存储并从与部署相同的域提供服务。

基本用法

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

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

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

指定字重

对于非可变字体,需要指定 weight

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

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

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

多个字重

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

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

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

可变字体

对于可变字体,不需要指定字重:

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

// 加载可变字体时,不需要指定字重
const inter = Inter({
  subsets: ['latin'],
  display: 'swap',
})

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

指定子集

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

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

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

本地字体

要使用本地字体,从 next/font/local 导入并指定字体文件的 src

单个字体文件

import localFont from 'next/font/local'

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

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

多个字体文件

如果要为单个字体系列使用多个文件,src 可以是数组:

import localFont from 'next/font/local'

const roboto = localFont({
  src: [
    {
      path: './Roboto-Regular.woff2',
      weight: '400',
      style: 'normal',
    },
    {
      path: './Roboto-Italic.woff2',
      weight: '400',
      style: 'italic',
    },
    {
      path: './Roboto-Bold.woff2',
      weight: '700',
      style: 'normal',
    },
    {
      path: './Roboto-BoldItalic.woff2',
      weight: '700',
      style: 'italic',
    },
  ],
})

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

可变本地字体

import localFont from 'next/font/local'

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

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

配置选项

subsets

字体子集数组,用于预加载。

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

weight

字体粗细,可以是字符串或字符串数组。

// 单个字重
const roboto = Roboto({
  weight: '400',
  subsets: ['latin'],
})

// 多个字重
const roboto = Roboto({
  weight: ['400', '700'],
  subsets: ['latin'],
})

style

字体样式,可以是字符串或字符串数组。

const roboto = Roboto({
  weight: '400',
  style: ['normal', 'italic'],
  subsets: ['latin'],
})

display

字体显示策略。

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // 'auto' | 'block' | 'swap' | 'fallback' | 'optional'
})

preload

布尔值,指定是否应预加载字体。默认为 true

const inter = Inter({
  subsets: ['latin'],
  preload: true,
})

variable

CSS 变量名称,用于与 CSS 变量一起使用。

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

adjustFontFallback

布尔值,是否自动使用后备字体来减少累积布局偏移。

const inter = Inter({
  subsets: ['latin'],
  adjustFontFallback: true, // 默认为 true
})

fallback

字体无法加载时使用的后备字体数组。

const inter = Inter({
  subsets: ['latin'],
  fallback: ['system-ui', 'arial'],
})

应用字体

使用 className

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

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

export default function Page() {
  return <h1 className={inter.className}>Hello, Next.js!</h1>
}

使用 style

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

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

export default function Page() {
  return <h1 style={inter.style}>Hello, Next.js!</h1>
}

使用 CSS 变量

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

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" className={inter.variable}>
      <body>{children}</body>
    </html>
  )
}
body {
  font-family: var(--font-inter);
}

多个字体

在同一文件中使用多个字体

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

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

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
      <body>{children}</body>
    </html>
  )
}
body {
  font-family: var(--font-inter);
}

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

在不同组件中使用不同字体

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

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

export default function Page() {
  return <h1 className={inter.className}>Home Page</h1>
}
import { Roboto } from 'next/font/google'

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

export default function About() {
  return <h1 className={roboto.className}>About Page</h1>
}

与 Tailwind CSS 集成

配置 Tailwind

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

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

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" className={`${inter.variable} ${robotoMono.variable}`}>
      <body>{children}</body>
    </html>
  )
}
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx,mdx}',
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './app/**/*.{js,ts,jsx,tsx,mdx}',
  ],
  theme: {
    extend: {
      fontFamily: {
        sans: ['var(--font-inter)'],
        mono: ['var(--font-roboto-mono)'],
      },
    },
  },
  plugins: [],
}

使用 Tailwind 类

export default function Page() {
  return (
    <div>
      <h1 className="font-sans">Hello with Inter</h1>
      <code className="font-mono">console.log('Hello')</code>
    </div>
  )
}

预加载

字体函数在网站页面上调用时,不会在所有路由上全局可用和预加载。相反,字体仅根据使用它的文件类型在相关路由上预加载:

  • 如果是唯一页面,则在该页面的唯一路由上预加载
  • 如果是布局,则在布局包装的所有路由上预加载
  • 如果是根布局,则在所有路由上预加载

示例:根布局预加载

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

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

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

在此示例中,Inter 字体将在所有路由上预加载。

示例:页面级预加载

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

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

export default function About() {
  return <h1 className={roboto.className}>About Page</h1>
}

在此示例中,Roboto 字体仅在 /about 路由上预加载。

重用字体定义

创建字体文件

import { Inter, Lora, Source_Code_Pro } from 'next/font/google'
import localFont from 'next/font/local'

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

export const lora = Lora({
  subsets: ['latin'],
  variable: '--font-lora',
})

export const sourceCodePro = Source_Code_Pro({
  subsets: ['latin'],
  variable: '--font-source-code-pro',
})

export const myLocalFont = localFont({
  src: './my-font.woff2',
  variable: '--font-my-local',
})

在多个文件中使用

import { inter } from '@/styles/fonts'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" className={inter.variable}>
      <body>{children}</body>
    </html>
  )
}
import { lora, sourceCodePro } from '@/styles/fonts'

export default function Page() {
  return (
    <div>
      <h1 className={lora.className}>Title in Lora</h1>
      <code className={sourceCodePro.className}>Code in Source Code Pro</code>
    </div>
  )
}

配置路径别名

{
  "compilerOptions": {
    "paths": {
      "@/fonts": ["./styles/fonts"]
    }
  }
}
import { inter, lora } from '@/fonts'

export default function About() {
  return (
    <div>
      <h1 className={inter.className}>About</h1>
      <p className={lora.className}>Description</p>
    </div>
  )
}

高级用法

条件字体加载

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

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

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

export default function RootLayout({
  children,
  params,
}: {
  children: React.ReactNode
  params: { locale: string }
}) {
  const fontClass = params.locale === 'zh' 
    ? notoSansSC.variable 
    : inter.variable

  return (
    <html lang={params.locale} className={fontClass}>
      <body>{children}</body>
    </html>
  )
}

字体回退

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

const inter = Inter({
  subsets: ['latin'],
  fallback: ['system-ui', 'arial'],
})

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

禁用预加载

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

const roboto = Roboto({
  weight: '400',
  subsets: ['latin'],
  preload: false, // 禁用预加载
})

export default function Page() {
  return <h1 className={roboto.className}>Page Title</h1>
}

最佳实践

1. 在根布局中定义全局字体

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

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

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

2. 使用 CSS 变量以获得更好的灵活性

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

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

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

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

3. 仅加载所需的子集

const inter = Inter({
  subsets: ['latin'], // 仅加载 latin 子集
})

4. 为可变字体省略字重

// 好的做法 - 可变字体
const inter = Inter({
  subsets: ['latin'],
})

// 不好的做法 - 可变字体不需要指定字重
const inter = Inter({
  weight: '400',
  subsets: ['latin'],
})

5. 重用字体定义

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

6. 使用 display: 'swap' 改善性能

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // 使用后备字体直到自定义字体加载完成
})

常见问题

字体未加载

确保字体名称正确,并且已指定所需的子集:

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

const inter = Inter({
  subsets: ['latin'], // 必须指定子集
})

布局偏移

使用 adjustFontFallback 选项:

const inter = Inter({
  subsets: ['latin'],
  adjustFontFallback: true, // 默认为 true
})

本地字体路径错误

确保路径相对于定义字体的文件:

// 如果字体文件在 app/fonts/my-font.woff2
// 并且在 app/layout.tsx 中定义
const myFont = localFont({
  src: './fonts/my-font.woff2', // 相对于 app/layout.tsx
})

多个字重不工作

对于非可变字体,使用数组指定多个字重:

const roboto = Roboto({
  weight: ['400', '700'], // 数组形式
  subsets: ['latin'],
})

总结

Next.js Font Optimization 提供了强大的字体优化功能:

  1. 自动优化:自动优化字体并移除外部网络请求
  2. 自托管:Google Fonts 自动自托管,无需向 Google 发送请求
  3. 零布局偏移:优化加载 Web 字体,防止布局偏移
  4. 灵活配置:支持 Google Fonts 和本地字体
  5. CSS 变量:与 CSS 变量和 Tailwind CSS 无缝集成
  6. 智能预加载:根据使用位置智能预加载字体
  7. 性能优化:字体在构建时下载并与静态资源一起托管

通过合理使用 next/font 模块,可以显著提升应用的性能、隐私和用户体验。

在 GitHub 上编辑

上次更新于