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