SSR服务端渲染 #
基本概念 #
服务端渲染(SSR)时,需要确保样式在服务端正确提取和注入,避免样式闪烁(FOUC)问题。Emotion 提供了完整的服务端渲染支持。
Next.js 集成 #
App Router (Next.js 13+) #
安装依赖:
bash
npm install @emotion/react @emotion/styled @emotion/cache
创建 Emotion 缓存配置:
jsx
import createCache from '@emotion/cache'
export default function createEmotionCache() {
return createCache({ key: 'css', prepend: true })
}
创建 Emotion Registry:
jsx
'use client'
import { CacheProvider } from '@emotion/react'
import createCache from '@emotion/cache'
import { useServerInsertedHTML } from 'next/navigation'
import { useState } from 'react'
export default function EmotionRegistry({ children }) {
const [cache] = useState(() => {
const cache = createCache({ key: 'css', prepend: true })
cache.compat = true
return cache
})
useServerInsertedHTML(() => {
return (
<style
data-emotion={`${cache.key} ${Object.keys(cache.inserted).join(' ')}`}
dangerouslySetInnerHTML={{
__html: Object.values(cache.inserted).join(' '),
}}
/>
)
})
return (
<CacheProvider value={cache}>
{children}
</CacheProvider>
)
}
在 app/layout.js 中使用:
jsx
import EmotionRegistry from './EmotionRegistry'
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<EmotionRegistry>
{children}
</EmotionRegistry>
</body>
</html>
)
}
Pages Router #
安装依赖:
bash
npm install @emotion/react @emotion/styled @emotion/cache @emotion/server
创建 _document.js:
jsx
import Document, { Html, Head, Main, NextScript } from 'next/document'
import createEmotionServer from '@emotion/server/create-instance'
import createEmotionCache from '../src/createEmotionCache'
export default class MyDocument extends Document {
render() {
return (
<Html lang="en">
<Head>
{this.props.emotionStyleTags}
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
MyDocument.getInitialProps = async (ctx) => {
const originalRenderPage = ctx.renderPage
const cache = createEmotionCache()
const { extractCriticalToChunks } = createEmotionServer(cache)
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) =>
function EnhanceApp(props) {
return <App emotionCache={cache} {...props} />
},
})
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,
}
}
创建 _app.js:
jsx
import { CacheProvider } from '@emotion/react'
import createEmotionCache from '../src/createEmotionCache'
const clientSideEmotionCache = createEmotionCache()
export default function App(props) {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props
return (
<CacheProvider value={emotionCache}>
<Component {...pageProps} />
</CacheProvider>
)
}
Gatsby 集成 #
安装插件:
bash
npm install gatsby-plugin-emotion @emotion/react @emotion/styled
配置 gatsby-config.js:
javascript
module.exports = {
plugins: [
{
resolve: `gatsby-plugin-emotion`,
options: {
sourceMap: true,
autoLabel: process.env.NODE_ENV !== 'production',
labelFormat: `[local]`,
cssPropOptimization: true,
},
},
],
}
自定义服务端渲染 #
Express 集成 #
jsx
import express from 'express'
import React from 'react'
import { renderToString } from 'react-dom/server'
import { CacheProvider } from '@emotion/react'
import createEmotionServer from '@emotion/server/create-instance'
import createCache from '@emotion/cache'
import App from './App'
const app = express()
app.get('*', (req, res) => {
const cache = createCache({ key: 'css' })
const { extractCritical } = createEmotionServer(cache)
const html = renderToString(
<CacheProvider value={cache}>
<App />
</CacheProvider>
)
const { css, ids } = extractCritical(html)
res.send(`
<!DOCTYPE html>
<html>
<head>
<style data-emotion-css="${ids.join(' ')}">${css}</style>
</head>
<body>
<div id="root">${html}</div>
<script>
window.__EMOTION_IDS__ = ${JSON.stringify(ids)};
</script>
<script src="/client.js"></script>
</body>
</html>
`)
})
app.listen(3000)
客户端水合 #
jsx
import { hydrate } from 'react-dom'
import { CacheProvider } from '@emotion/react'
import createCache from '@emotion/cache'
import App from './App'
const cache = createCache({ key: 'css' })
cache.compat = true
const ids = window.__EMOTION_IDS__
hydrate(
<CacheProvider value={cache}>
<App />
</CacheProvider>,
document.getElementById('root')
)
样式提取 #
extractCritical #
提取关键样式:
jsx
import createEmotionServer from '@emotion/server/create-instance'
import createCache from '@emotion/cache'
const cache = createCache({ key: 'css' })
const { extractCritical } = createEmotionServer(cache)
const { html, css, ids } = extractCritical(renderedHtml)
extractCriticalToChunks #
将样式分块提取:
jsx
import createEmotionServer from '@emotion/server/create-instance'
import createCache from '@emotion/cache'
const cache = createCache({ key: 'css' })
const { extractCriticalToChunks } = createEmotionServer(cache)
const result = extractCriticalToChunks(renderedHtml)
result.styles.forEach((style) => {
console.log(style.key)
console.log(style.ids)
console.log(style.css)
})
流式渲染 #
renderToStream #
支持流式渲染:
jsx
import { renderToPipeableStream } from 'react-dom/server'
import { CacheProvider } from '@emotion/react'
import createEmotionServer from '@emotion/server/create-instance'
import createCache from '@emotion/cache'
function renderApp(res, App) {
const cache = createCache({ key: 'css' })
const { extractCriticalToChunks, constructStyleTagsFromChunks } =
createEmotionServer(cache)
const stream = renderToPipeableStream(
<CacheProvider value={cache}>
<App />
</CacheProvider>,
{
onShellReady() {
res.statusCode = 200
res.setHeader('Content-Type', 'text/html')
stream.pipe(res)
},
}
)
}
缓存配置 #
基本配置 #
jsx
import createCache from '@emotion/cache'
const cache = createCache({
key: 'my-app',
prepend: true,
speedy: true,
container: document.head,
})
配置选项 #
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| key | string | ‘css’ | 类名前缀 |
| prepend | boolean | false | 是否前置插入样式 |
| speedy | boolean | true | 使用快速模式 |
| container | HTMLElement | document.head | 样式容器 |
| nonce | string | - | CSP nonce |
| stylisPlugins | array | - | Stylis 插件 |
Stylis 插件 #
前缀插件 #
jsx
import createCache from '@emotion/cache'
import prefixer from 'stylis-plugin-prefixer'
const cache = createCache({
key: 'css',
stylisPlugins: [prefixer],
})
自定义插件 #
jsx
const myPlugin = (context, content, selectors, parents, line, column, length, type) => {
if (context === 2) {
return content.replace(/custom-property/g, 'replacement')
}
}
const cache = createCache({
key: 'css',
stylisPlugins: [myPlugin],
})
常见问题 #
1. 样式闪烁 #
确保正确提取和注入样式:
jsx
const { html, css, ids } = extractCritical(renderedHtml)
res.send(`
<style data-emotion-css="${ids.join(' ')}">${css}</style>
${html}
`)
2. 类名不匹配 #
确保客户端和服务端使用相同的缓存 key:
jsx
const cacheKey = 'my-app'
const serverCache = createCache({ key: cacheKey })
const clientCache = createCache({ key: cacheKey })
3. 全局样式问题 #
在服务端正确处理全局样式:
jsx
import { Global, css } from '@emotion/react'
function App() {
return (
<>
<Global
styles={css`
body {
margin: 0;
}
`}
/>
<Content />
</>
)
}
最佳实践 #
1. 共享缓存配置 #
jsx
export const createEmotionCache = () => {
return createCache({ key: 'css', prepend: true })
}
2. 类型安全 #
typescript
import createCache from '@emotion/cache'
import { EmotionCache } from '@emotion/react'
export const createEmotionCache = (): EmotionCache => {
return createCache({ key: 'css', prepend: true })
}
3. 错误处理 #
jsx
app.get('*', (req, res, next) => {
try {
const html = renderApp()
res.send(html)
} catch (error) {
next(error)
}
})
下一步 #
掌握了 SSR 配置后,继续学习 缓存与性能,了解 Emotion 的性能优化技巧。
最后更新:2026-03-28