
在 next.js 中直接将 `next/font` 的 `classname` 应用于 `` 标签会引发服务端与客户端渲染不一致,从而触发 hydration 错误;正确做法是将字体类名应用于客户端可稳定挂载的 dom 容器(如 `
` 下的根 `Next.js 的 next/font(尤其是 google 子模块)采用零运行时、自动内联 CSS 的优化策略,其生成的 className(如 lexend.className)实际包含一个动态注入的 <style> 标签和唯一哈希类名。该机制依赖于 客户端 hydration 时的 DOM 挂载时机 —— 若将该类名直接绑定到 <html> 元素上,而该元素由 Next.js 服务端预渲染(SSR)静态生成,则服务端无法提前知道客户端计算出的最终类名(因字体加载逻辑含客户端检测),导致 HTML 字符串不匹配,进而抛出经典的 Hydration error:
Error: Hydration failed because the initial UI does not match what was rendered on the server.
✅ 正确实践:永远不要将 next/font 的 className 应用于 <html> 或 <body> 标签(它们由 Next.js 自动管理,且服务端无权修改)。应将其作用于应用级布局容器,例如 app/layout.tsx 中的 <body> 内部根 <div>:
// app/layout.tsx
import { Lexend } from 'next/font/google';
const lexend = Lexend({ subsets: ['latin'] });
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={lexend.className}> {/* ✅ 正确:作用于 body */}
<div className="min-h-screen flex flex-col">
{children}
</div>
</body>
</html>
);
}⚠️ 注意事项:
- next/font 必须在 Root Layout(即 app/layout.tsx)中声明并传入 className,不可在页面组件(如 app/page.tsx)或客户端组件中局部调用后试图覆盖 <html>;
- 若使用 app/ 目录结构,<html> 和 <body> 标签仅允许出现在 layout.tsx 中,且 lang、className 等属性必须由服务端可确定的值驱动;
- Lexend({ subsets: ['latin'] }) 返回的 className 是一个 React Hook 调用结果(内部含 useEffect),因此不能在服务端组件中直接解构使用——但 next/font 已对此做了封装,你只需在服务端组件中调用并传入 className 即可,框架会自动处理 SSR/CSR 衔接;
- 如需多字体组合,可链式声明多个字体实例,并合并类名:
const lexend = Lexend({ subsets: ['latin'] }); const inter = Inter({ subsets: ['latin'] }); // 使用时:className={`${lexend.className} ${inter.variable}`}
? 额外提示:若项目需严格控制字体加载行为(如避免 FOIT/FOUT),可结合 display 属性(如 'swap', 'optional')进一步优化:
const lexend = Lexend({
subsets: ['latin'],
display: 'swap', // 推荐:立即显示备用字体,加载完成即切换
});总结:Hydration 错误的本质是服务端与客户端对同一 DOM 节点的属性值预期不一致。next/font 的设计哲学是“服务端生成字体 CSS,客户端注入并激活类名”,因此务必让 className 落在服务端能静态输出、客户端能安全接管的容器上——<body> 下的包裹 <div> 就是最稳妥的选择。


















