不能在 _document.js 中直接使用 ServerStyleSheet 预取样式,因其仅在 Pages Router 中有效且须在 getInitialProps 内创建、收集并注入;App Router 不支持此方式。

不能在 _document.js 里直接用 styled-components 的 ServerStyleSheet 做样式预取 —— 这是 Next.js 13+ App Router 的限制,而你如果还在用 Pages Router,_document.js 确实是唯一能注入 SSR 样式的地方,但必须严格遵循生命周期和序列化规则。
为什么 _document.js 中的 ServerStyleSheet 容易失效
Next.js 的 _document.js 不是 React 组件渲染上下文,它只在服务端运行一次,用于生成 HTML 模板。如果你在 getInitialProps 外部初始化 ServerStyleSheet,或在 render() 里调用 .collectStyles(),样式根本不会被收集;更常见的是:你用了 babel-plugin-styled-components 但没关掉 ssr: false,导致客户端样式重复注入、FOUC 或 hydrate mismatch。
-
ServerStyleSheet必须在getInitialProps内创建、收集、转为css字符串,再注入html的head - 所有页面组件(包括
_app.js)必须用styled-components的StyleSheetManager+disableVendorPrefixes: true配合服务端生成的css - 如果用了
next/dynamic动态导入组件,需确保这些组件内部不触发新的useEffect样式注入 —— 否则预取的 CSS 里没有它们
_document.js 的最小可行配置(Pages Router)
以下代码适用于 Next.js 12–13(Pages Router),不兼容 App Router。注意:必须用 getInitialProps,且不能解构 ctx 后丢掉 renderPage 调用。
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';
<p>export default class MyDocument extends Document {
static async getInitialProps(ctx) {
const sheet = new ServerStyleSheet();
const originalRenderPage = ctx.renderPage;</p><pre class="brush:php;toolbar:false;">try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
return {
...initialProps,
styles: [
<>{initialProps.styles}</>
{sheet.getStyleElement()},
],
};
} finally {
sheet.seal();
}} }
关键点:sheet.collectStyles() 包裹的是整个 App 树,不是单个组件;sheet.seal() 必须在 finally 块中调用,否则热重载时会泄漏样式实例。
配合 _app.js 防止客户端样式重复
仅靠 _document.js 收集还不够。客户端 hydration 时,styled-components 默认会重新计算并插入样式,导致重复、闪烁甚至布局偏移。你需要显式禁用客户端样式生成,并复用服务端传来的 CSS。
- 在
_app.js中用StyleSheetManager并传入target: null,让客户端不操作 DOM - 确保
getServerSideProps或getStaticProps没有返回__NEXT_DATA__.props.pageProps里带内联<style></style>的字段(会被误解析) - 检查浏览器开发者工具 Elements 面板:最终
应只有 **一个**<style data-styled="..."></style>标签,且其内容与服务端输出一致
示例 _app.js 片段:
import { StyleSheetManager } from 'styled-components';
import { useEffect } from 'react';
<p>function MyApp({ Component, pageProps, emotionCache }) {
// 注意:这里不 new ServerStyleSheet,只用 StyleSheetManager 控制目标
return (
<StyleSheetManager target={null}>
<Component {...pageProps} />
</StyleSheetManager>
);
}</p><p>export default MyApp;验证是否真正“预取”成功
所谓“样式预取”,本质是把关键 CSS 提前塞进 HTML 的 ,避免客户端 JS 加载后才注入。验证方式很直接:
- 禁用 JS 后打开页面:文字/布局应基本可读(无严重错位),说明关键样式已内联
- 查看网络面板 → HTML 响应体,搜索
data-styled,确认有非空的<style></style>标签 - 对比开启/关闭
babel-plugin-styled-components的ssr: true选项:关闭后,_document.js收集到的样式会为空 - 如果用了
styled-reset或全局createGlobalStyle,确保它们被collectStyles包裹 —— 否则不会出现在预取结果中
最容易被忽略的是:开发环境下热重载会绕过 _document.js 的 getInitialProps,所以必须在生产构建(next build && next start)后验证,而不是 next dev。


















