Material UI + Next.js 集成实践:从 Emotion 缓存、next/font 到 CSS 层与 Link 适配
Material UI 基于 Emotion 组织样式,在 Next.js 的服务端渲染(SSR)与流式渲染场景下,必须先把 Emotion cache「接线」妥当,才能让 CSS 按预期注入 <head>。本篇文章以仓库内 skills/material-ui-nextjs/AGENTS.md 技能文档及其对应的 Next.js 集成指南 为主体,完整讲解 App Router 与 Pages Router 两套接入路径、@mui/material-nextjs 的 Provider 用法、next/font 主题化、CSS theme variables 的 SSR 注意事项,以及与 Tailwind/CSS Modules 通过 @layer 共存、Button component={Link} 的适配方法,帮助你在搭建或排查 Next.js 项目中的 MUI 渲染问题时一步到位。
为什么需要 @mui/material-nextjs
Material UI 的样式引擎 Emotion 默认在浏览器端把生成的 CSS 插入页面。但在 Next.js 中,HTML 是在服务端渲染并通过流式(streaming)分块推送给客户端的,若不在服务端收集 Emotion 生成的 CSS,就会导致样式缺失、顺序错乱,或样式被注入 <body> 而非 <head>。@mui/material-nextjs 包正是为此提供开箱即用的缓存 Provider:
- App Router:
AppRouterCacheProvider——在服务端渲染与流式输出过程中收集 MUI System 生成的 CSS,让样式稳定挂载在<head>; - Pages Router:
AppCacheProvider+DocumentHeadTags——在_document阶段抽取样式标签,在_app中承接客户端缓存。
从源码结构看,当前仓库 packages/mui-material-nextjs 内的版本为 9.4.0,与 Material UI v9 保持同步;技能文档亦声明其目标版本为 >=9.0.0 <10.0.0(见 metadata.json)。
一个容易被忽略的前提是:Material UI 组件以客户端组件("use client")形式分发,它们依然参与 SSR,但并非 React Server Components;同时 @mui/material-nextjs 的导入路径带 v1X- 前缀(如 v15-appRouter、v16-appRouter),必须与所用 Next.js 大版本匹配。
App Router 接入(推荐路径)
1. 安装依赖
确保项目已安装 @mui/material 与 next,再追加两个包:
pnpm add @mui/material-nextjs @emotion/cache
App Router 默认只需要 @emotion/cache;@emotion/server 是 Pages Router 场景才需要的。
2. 在根布局中包裹 AppRouterCacheProvider
在 app/layout.tsx 中,将 <body> 之下的所有内容用 AppRouterCacheProvider 包裹,并从与 Next 大版本匹配的入口导入:
import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
// 若不是 Next 15,使用对应的 v1X-appRouter,如 v14-appRouter、v16-appRouter
export default function RootLayout(props) {
return (
<html lang="en">
<body>
<AppRouterCacheProvider>{props.children}</AppRouterCacheProvider>
</body>
</html>
);
}
仓库示例 examples/material-ui-nextjs/src/app/layout.js 展示了完整形态——它基于 Next 16 从 @mui/material-nextjs/v16-appRouter 导入,并在 Provider 内依次放入 ThemeProvider、CssBaseline 再渲染 children。
:::note 为什么不加会出问题?
AppRouterCacheProvider 负责在服务端渲染时收集 MUI System 生成的 CSS。不使用它通常仍可运行,但样式可能只出现在 <body> 中;把它加上后,样式才会稳定进入 <head>。
:::
3. 可选:自定义缓存 options
AppRouterCacheProvider 接受 options prop,可覆盖 Emotion 的默认缓存选项。最常见的调整是修改样式插入时的 CSS key——MUI 默认 key 为 mui,可改成 css:
<AppRouterCacheProvider options={{ key: 'css' }}>
{children}
</AppRouterCacheProvider>
该 options 直接透传给 Emotion cache 的选项(非本仓库内资源,作外围理解即可),因此你还可以传入 nonce、insertionPoint 等 Emotion 支持的字段。
4. URL 状态与 Suspense 边界
仪表盘、内部工具类应用常常用 MUI 客户端组件(Table、Tabs、TextField 等)承载筛选、标签页、分页等由 URL 驱动的 UI,并通过 next/navigation 的 useSearchParams() 读取查询串。Next.js 要求:凡是读取 URL、使路由进入客户端渲染状态的代码,其外层必须有 <Suspense> 边界,否则构建或运行时可能报「missing Suspense boundary」之类的错误(行为随 Next.js 版本与静态/动态渲染配置而异)。
推荐的工程结构是:
- 尽可能让
app/.../page.tsx保持为服务端组件; - 把真正调用
useSearchParams、渲染 MUI 组件的子树拆成独立客户端文件; - 在服务端 page 中用
<Suspense>包裹该客户端子树。
注意 fallback 不能随意用 null:对于占据布局空间的工具条、筛选栏等 UI,空 fallback 会让首屏流式 HTML 缺一块,待客户端 hydration 后才突然出现,造成明显的布局抖动(CLS)。应使用与最终 UI 尺寸、结构接近的占位内容,例如用 MUI 的 Skeleton 配合 Stack/Box,并设置相同的 minHeight、弹性方向与断点:
import { Suspense } from 'react';
import Box from '@mui/material/Box';
import Skeleton from '@mui/material/Skeleton';
import Stack from '@mui/material/Stack';
import OrdersToolbar from './OrdersToolbar';
function OrdersToolbarFallback() {
return (
<Stack
direction="row"
spacing={2}
useFlexGap
sx={{ flexWrap: 'wrap', alignItems: 'center', minHeight: 56 }}
>
<Skeleton variant="rounded" height={40} sx={{ minWidth: 200, flexGrow: { xs: 1, sm: 0 } }} />
<Skeleton variant="rounded" width={120} height={40} />
<Box sx={{ flexGrow: 1 }} />
<Skeleton variant="rounded" width={100} height={40} />
</Stack>
);
}
export default function Page() {
return (
<Suspense fallback={<OrdersToolbarFallback />}>
<OrdersToolbar />
</Suspense>
);
}
其中 OrdersToolbar 是标注了 'use client'、内部调用 useSearchParams() 的组件。请按真实工具条(或筛选行)的布局微调 fallback 的尺寸。
Pages Router 接入
Pages Router 的接线分布在两个文件:_document 负责服务端抽取样式,_app 负责客户端缓存承接。
1. 安装依赖
pnpm add @mui/material-nextjs @emotion/cache @emotion/server
2. pages/_document.tsx
- 从
v1X-pagesRouter入口导入DocumentHeadTags与documentGetInitialProps; - 在
<Head>内渲染<DocumentHeadTags {...props} />; - 把 Document 的
getInitialProps指派为documentGetInitialProps:
import { DocumentHeadTags, documentGetInitialProps } from '@mui/material-nextjs/v15-pagesRouter';
// 或使用匹配的 v1X-pagesRouter
export default function MyDocument(props) {
return (
<Html lang="en">
<Head>
<DocumentHeadTags {...props} />
...
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
MyDocument.getInitialProps = async (ctx) => {
const finalProps = await documentGetInitialProps(ctx);
return finalProps;
};
DocumentHeadTags 会把服务端收集到的样式标签(Emotion <style>)渲染进 <Head>,这是样式进入 <head> 的关键。
3. pages/_app.tsx
用同一大版本入口导入的 AppCacheProvider 作为根元素包裹应用:
import { AppCacheProvider } from '@mui/material-nextjs/v15-pagesRouter';
export default function MyApp(props) {
return (
<AppCacheProvider {...props}>
<Head>...</Head>
...
</AppCacheProvider>
);
}
AppCacheProvider 负责在客户端为 hydration 提供一致的缓存实例,服务端渲染时收集的样式标签才不会与客户端重新生成的样式打架。
4. 自定义缓存与级联层(@layer)
Pages Router 自定义缓存的传入口在 documentGetInitialProps 的选项里:把自定义 Emotion cache 作为 emotionCache 传入。若要开启 @layer,则使用 @mui/material-nextjs 提供的 createEmotionCache({ enableCssLayer: true }),并保证 _document 与 _app 使用同一套缓存模式:
// pages/_document.tsx
import { createEmotionCache } from '@mui/material-nextjs/v15-pagesRouter';
MyDocument.getInitialProps = async (ctx) => {
const finalProps = await documentGetInitialProps(ctx, {
emotionCache: createEmotionCache({ enableCssLayer: true }),
});
return finalProps;
};
// pages/_app.tsx
import { createEmotionCache } from '@mui/material-nextjs/v15-pagesRouter';
const clientCache = createEmotionCache({ enableCssLayer: true });
export default function MyApp({ emotionCache = clientCache }) {
return <AppCacheProvider emotionCache={emotionCache}>{/* ... */}</AppCacheProvider>;
}
5. 可选:App 增强插件(plugins)
documentGetInitialProps 还支持 plugins 数组,用于叠加其他 SSR 样式方案(如 JSS、styled-components)。每个插件需提供两个属性:
enhanceApp:接收App组件、返回新 App 组件的高阶组件;resolveProps:接收 initial props、返回新 props 对象的函数。
执行顺序是:先按顺序执行所有插件的 enhanceApp,再按同样顺序执行 resolveProps。下面是一个同时接入 styled-components 与 JSS 的完整示例:
import { ServerStyleSheet } from 'styled-components';
MyDocument.getInitialProps = async (ctx) => {
const jssSheets = new JSSServerStyleSheets();
const styledComponentsSheet = new ServerStyleSheet();
try {
const finalProps = await documentGetInitialProps(ctx, {
emotionCache: createEmotionCache(),
plugins: [
{
// styled-components
enhanceApp: (App) => (props) => styledComponentsSheet.collectStyles(<App {...props} />),
resolveProps: async (initialProps) => ({
...initialProps,
styles: [styledComponentsSheet.getStyleElement(), ...initialProps.styles],
}),
},
{
// JSS
enhanceApp: (App) => (props) => jssSheets.collect(<App {...props} />),
resolveProps: async (initialProps) => {
const css = jssSheets.toString();
return {
...initialProps,
styles: [
...initialProps.styles,
<style id="jss-server-side" key="jss-server-side" dangerouslySetInnerHTML={{ __html: css }} />,
<style id="insertion-point-jss" key="insertion-point-jss" />,
],
};
},
},
],
});
return finalProps;
} finally {
styledComponentsSheet.seal();
}
};
6. TypeScript 类型
TS 项目需把 DocumentHeadTagsProps 并入 Document 组件的 props 类型(从同一导入路径取):
import type { DocumentHeadTagsProps } from '@mui/material-nextjs/v15-pagesRouter';
export default function MyDocument(props: DocumentProps & DocumentHeadTagsProps) {
...
}
可对照仓库现成的 Pages Router + TypeScript 示例 examples/material-ui-nextjs-pages-router-ts 进行整体参照。
字体:next/font 与主题变量
Next.js 自带字体优化(next/font),自托管字体可避免布局偏移。接入 MUI 的关键是:主题模块若被服务端组件消费,需要 'use client' 指令;字体 CSS 变量挂在 <html> 的 className 上;主题里用 var(--font-…) 引用该变量。
App Router 的标准做法如下。第一步,新建 src/theme.ts,使用 var(--font-roboto) 作为 typography.fontFamily:
'use client';
import { createTheme } from '@mui/material/styles';
const theme = createTheme({
typography: {
fontFamily: 'var(--font-roboto)',
},
});
export default theme;
第二步,在 app/layout.tsx 加载字体、把 variable 设到 <html className> 上,并用 ThemeProvider 注入主题:
import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
import { Roboto } from 'next/font/google';
import { ThemeProvider } from '@mui/material/styles';
import theme from '../theme';
const roboto = Roboto({
weight: ['300', '400', '500', '700'],
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto',
});
export default function RootLayout(props) {
const { children } = props;
return (
<html lang="en" className={roboto.variable}>
<body>
<AppRouterCacheProvider>
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</AppRouterCacheProvider>
</body>
</html>
);
}
Provider 的层级关系可以归纳为(详见 reference.md):
<html>
<body>
<AppRouterCacheProvider>
<ThemeProvider theme={theme}>
{children}
</ThemeProvider>
</AppRouterCacheProvider>
</body>
</html>
Pages Router 是同样的思路,只是放在 pages/_app.tsx:加载字体后用 AppCacheProvider 包裹,再套 ThemeProvider,并把 roboto.variable 放到 <main className={roboto.variable}>(或 html 上)即可。
CSS theme variables 与 SSR 防闪烁
当使用 CSS theme variables 体系(把设计令牌输出为 CSS 变量)时,需要在 createTheme 中开启 cssVariables: true:
'use client';
const theme = createTheme({
cssVariables: true,
});
由于 colorSchemes(明/暗模式)属性是客户端在首次渲染时写进 DOM 的,若 <html> 上缺少对应属性,会产生 React hydration mismatch。因此:
- 在
<html>上添加suppressHydrationWarning; - 若要避免 SSR 阶段明暗主题闪烁(先亮后暗),按 CSS theme variables 文档中「Preventing SSR flickering」的方案,尽早执行
InitColorSchemeScript。
colorSchemes 与 defaultMode 的完整说明见 docs/data/material/customization 目录下的 CSS theme variables 主题文档(skills 中亦有指针),原理是服务端先行输出 data-color-scheme 标记,客户端 hydration 前不产生可见跳变。
与其他样式方案共存:enableCssLayer
如果你同时使用 Tailwind CSS、CSS Modules 或其他全局 CSS 来定制 MUI 组件,直接让两套样式互相覆盖会有「谁后加载谁赢」的不确定性。MUI 的解法是把其输出包进 CSS 级联层 @layer mui——因为匿名层(Tailwind、CSS Modules、普通 CSS)优先级高于具名层,全局样式即可按预期覆盖 MUI 默认样式。
App Router 只需一行:
<AppRouterCacheProvider options={{ enableCssLayer: true }}>
Pages Router 则在 _document 与 _app 中共同使用 createEmotionCache({ enableCssLayer: true })(见上文级联层一节)。仓库的 App Router 示例 examples/material-ui-nextjs/src/app/layout.js 正是开启 options={{ enableCssLayer: true }} 的写法。关于 @layer 语义可参考 MDN 的 CSS @layer 文档(非本仓库资源,此处仅为概念背景)。
Link 与 component prop:避开 Next.js v16 的 Client 边界限制
给 MUI 组件(如 Button)传入 component={Link} 是常见的路由适配手法。但 Next.js v16 开始,直接把 next/link 的默认导出塞进 component 会触发 “Functions cannot be passed directly to Client Components” 错误。解决办法是做一个带 'use client' 的小包装再导出,仓库示例 examples/material-ui-nextjs/src/components/Link.js 正是这种形态:
'use client';
import Link, { LinkProps } from 'next/link';
export default Link;
然后替换页面里的导入来源并使用 component prop:
import Link from '../components/Link';
// ...
<Button component={Link} href="/about" variant="contained">
Go to About Page
</Button>
主题级、页面级的整体 Link 适配思路可参考仓库内 docs/data/material/integrations 下的路由集成文档以及 Pages Router 示例。
小结与进一步阅读
接入工作可以浓缩为三个判断:
- 样式去向:是否用
AppRouterCacheProvider/AppCacheProvider + DocumentHeadTags把 SSR 与流式阶段的 Emotion CSS 送进<head>; - 导入路径:
@mui/material-nextjs/v1X-appRouter或v1X-pagesRouter的后缀是否与 Next.js 大版本一致; - 边界与共存:URL 驱动的 MUI 子树是否包了
<Suspense>(且 fallback 不塌陷布局),与其他 CSS 方案共存时是否开启enableCssLayer,以及字体/主题变量、Link 包装是否就位。
若要在实际仓库中进一步对照与排查,推荐阅读:
- 技能完整指南:AGENTS.md(本技能文档主文件,面向 Agent/LLM 的完整说明)
- 技能入口与索引:SKILL.md、导入路径速查 reference.md
- 官方集成指南源码版:docs/data/material/integrations/nextjs/nextjs.md
- App Router + TypeScript 示例:examples/material-ui-nextjs-ts,App Router JS 示例 examples/material-ui-nextjs(其
layout.js与Link.js均可在仓库直接查看) - Pages Router + TypeScript 示例:examples/material-ui-nextjs-pages-router-ts
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00