Next.js升级React17至React18后SSR不渲染CSS解决方案
问题共性说明
这是Next.js项目从React 17升级React 18时,搭配MUI+Emotion做SSR的高频常见问题,大量开发者升级后都遇到过首屏样式丢失、样式闪烁、hydration阶段样式错乱的问题。
核心根因
现有代码存在三个和React 18不兼容的点:
- 仅在
_document.tsx的服务端渲染阶段注入了Emotion缓存,客户端hydration阶段没有复用同规则的缓存实例,导致客户端生成的CSS类名和服务端输出的类名不匹配 _app.tsx里残留了MUI v4(JSS版本)的服务端样式清理逻辑,React 18下useEffect执行时机和React 17存在差异,会在hydration未完成时提前删除服务端注入的样式,且当前用的Emotion方案根本不会生成id为jss-server-side的标签,这段逻辑本身已经失效_document.tsx里的服务端样式收集逻辑没有适配React 18的新渲染机制,提取的关键CSS不完整,导致首屏加载时部分样式缺失
分步修复方案
1. 修改_app.tsx文件
删除旧的JSS样式清理逻辑,在客户端侧也包裹Emotion的CacheProvider,和服务端使用完全一致的缓存配置,修改后代码参考:
import { CacheProvider } from "@emotion/react"; import { AppProvider } from "contexts/AppContext"; import SettingsProvider from "contexts/SettingContext"; import { NextPage } from "next"; import { AppProps } from "next/app"; import Head from "next/head"; import Router from "next/router"; import { Fragment, ReactElement, ReactNode } from "react"; import MuiTheme from "theme/MuiTheme"; import createEmotionCache from "src/createEmotionCache"; // 客户端侧单独创建缓存实例,全局复用 const clientSideEmotionCache = createEmotionCache(); type MyAppProps = AppProps & { Component: NextPage & { getLayout?: (page: ReactElement) => ReactNode; }; emotionCache?: ReturnType<typeof createEmotionCache>; }; const App = ({ Component, pageProps, emotionCache = clientSideEmotionCache }: MyAppProps) => { const getLayout = Component.getLayout ?? ((page) => page); return ( <CacheProvider value={emotionCache}> <Fragment> <SettingsProvider> <AppProvider> <MuiTheme> {getLayout(<Component {...pageProps} />)} </MuiTheme> </AppProvider> </SettingsProvider> </Fragment> </CacheProvider> ); }; export default App;
2. 调整_document.tsx的样式注入逻辑
确保服务端收集到的Emotion样式插入位置正确,不会被客户端加载的样式覆盖,核心修改点是把收集到的样式标签放在Head组件内渲染,修改后关键代码参考:
import { CacheProvider } from "@emotion/react"; import createEmotionServer from "@emotion/server/create-instance"; import Document, { Head, Html, Main, NextScript } from "next/document"; import React from "react"; import createEmotionCache from "../src/createEmotionCache"; export default class Bazar extends Document { render() { return ( <Html lang="en"> <Head> <link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700;900&display=swap" rel="stylesheet" /> <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" /> {/* 直接在此处注入服务端收集的Emotion样式 */} {this.props.emotionStyleTags} </Head> <body> <Main /> <NextScript /> </body> </Html> ); } } Bazar.getInitialProps = async (ctx) => { const originalRenderPage = ctx.renderPage; const cache = createEmotionCache(); const { extractCriticalToChunks } = createEmotionServer(cache); ctx.renderPage = () => originalRenderPage({ enhanceApp: (App) => (props) => ( <CacheProvider value={cache}> <App {...props} /> </CacheProvider> ), }); const initialProps = await Document.getInitialProps(ctx); const emotionStyles = extractCriticalToChunks(initialProps.html); const emotionStyleTags = emotionStyles.styles.map((style) => ( <style data-emotion={`${style.key} ${style.ids.join(" ")}`} key={style.key} dangerouslySetInnerHTML={{ __html: style.css }} /> )); return { ...initialProps, emotionStyleTags, }; };
3. 校验依赖版本兼容性
确保相关依赖版本满足React 18适配要求,避免版本不兼容导致的异常:
react、react-dom版本 >= 18.2.0@emotion/react、@emotion/styled、@emotion/server版本 >= 11.10.0- MUI相关包(
@mui/material、@mui/system等)版本 >=5.10.0 - Next.js版本建议升级到12.3.0及以上,对React 18的hydration、SSR逻辑有完整兼容
验证方式
注意:不要在dev开发模式下验证SSR样式问题,dev模式下样式为动态注入,和生产环境SSR逻辑完全不同,验证结果不具备参考性
修复完成后执行生产构建校验:
- 执行
next build完成生产环境构建 - 执行
next start启动生产模式服务 - 访问页面对照检查:首屏加载无样式闪烁,控制台无hydration mismatch、CSS类名不匹配相关警告,查看页面源代码可看到head节点中存在带
data-emotion属性的服务端注入样式标签
内容的提问来源于stack exchange,提问作者Nabed Khan
相关产品推荐
相关产品推荐

