
在 next.js 中直接将 `next/font` 的 `classname` 应用于 `html>` 标签会引发服务端与客户端渲染不一致,从而触发 hydration 错误;正确做法是将字体类名应用于客户端可稳定挂载的 dom 容器(如 `
` 下的根 `Next.js 的 next/font(尤其是 google 子模块)采用零运行时、自动内联 CSS 的优化策略,其生成的 className(如 lexend.className)实际包含一个动态注入的
Error: Hydration failed because the initial UI does not match what was rendered on the server.
✅ 正确实践:永远不要将 next/font 的 className 应用于 或 标签(它们由 Next.js 自动管理,且服务端无权修改)。应将其作用于应用级布局容器,例如 app/layout.tsx 中的
内部根// app/layout.tsx
import { Lexend } from 'next/font/google';
const lexend = Lexend({ subsets: ['latin'] });
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{/* ✅ 正确:作用于 body */}
{children}
);
}⚠️ 注意事项:
- next/font 必须在 Root Layout(即 app/layout.tsx)中声明并传入 className,不可在页面组件(如 app/page.tsx)或客户端组件中局部调用后试图覆盖 ;
- 若使用 app/ 目录结构, 和 标签仅允许出现在 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 落在服务端能静态输出、客户端能安全接管的容器上——
下的包裹










