服务端渲染 #

一、SSR 基础 #

1.1 为什么需要 SSR 样式处理 #

text
SSR 样式问题
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│  客户端渲染 (CSR)                                           │
│  ├── HTML 先加载                                            │
│  ├── JavaScript 执行                                        │
│  └── 样式后注入 → FOUC (闪烁)                               │
│                                                             │
│  服务端渲染 (SSR)                                           │
│  ├── HTML 包含样式                                          │
│  ├── 首屏即渲染完整样式                                     │
│  └── 无闪烁,更好的 SEO                                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

1.2 ServerStyleSheet #

jsx
import { renderToString } from 'react-dom/server';
import { ServerStyleSheet } from 'styled-components';

function renderApp() {
  const sheet = new ServerStyleSheet();
  
  try {
    const html = renderToString(
      sheet.collectStyles(<App />)
    );
    const styleTags = sheet.getStyleTags();
    
    return `
      <!DOCTYPE html>
      <html>
        <head>
          ${styleTags}
        </head>
        <body>
          <div id="root">${html}</div>
          <script src="/client.js"></script>
        </body>
      </html>
    `;
  } finally {
    sheet.seal();
  }
}

二、Next.js 集成 #

2.1 Next.js 配置 #

javascript
const nextConfig = {
  compiler: {
    styledComponents: true,
  },
};

module.exports = nextConfig;

2.2 App Router 配置 #

tsx
import React from 'react';
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
import { useServerInsertedHTML } from 'next/navigation';

export default function StyledComponentsRegistry({
  children,
}: {
  children: React.ReactNode;
}) {
  const [styledComponentsStyleSheet] = React.useState(
    () => new ServerStyleSheet()
  );

  useServerInsertedHTML(() => {
    const styles = styledComponentsStyleSheet.getStyleElement();
    styledComponentsStyleSheet.instance.clearTag();
    return <>{styles}</>;
  });

  if (typeof window !== 'undefined') {
    return <>{children}</>;
  }

  return (
    <StyleSheetManager sheet={styledComponentsStyleSheet.instance}>
      {children}
    </StyleSheetManager>
  );
}

2.3 Layout 配置 #

tsx
import StyledComponentsRegistry from './lib/registry';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <StyledComponentsRegistry>
          {children}
        </StyledComponentsRegistry>
      </body>
    </html>
  );
}

三、自定义 SSR 配置 #

3.1 Express 集成 #

jsx
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
import App from './App';

const app = express();

app.get('*', (req, res) => {
  const sheet = new ServerStyleSheet();
  
  try {
    const html = renderToString(
      <StyleSheetManager sheet={sheet.instance}>
        <App />
      </StyleSheetManager>
    );
    
    const styleTags = sheet.getStyleTags();
    
    const document = `
      <!DOCTYPE html>
      <html>
        <head>
          <title>My App</title>
          ${styleTags}
        </head>
        <body>
          <div id="root">${html}</div>
          <script src="/client.js"></script>
        </body>
      </html>
    `;
    
    res.send(document);
  } catch (error) {
    console.error(error);
    res.status(500).send('Error');
  } finally {
    sheet.seal();
  }
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

3.2 Koa 集成 #

jsx
import Koa from 'koa';
import { renderToString } from 'react-dom/server';
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
import App from './App';

const app = new Koa();

app.use(async (ctx) => {
  const sheet = new ServerStyleSheet();
  
  try {
    const html = renderToString(
      <StyleSheetManager sheet={sheet.instance}>
        <App />
      </StyleSheetManager>
    );
    
    const styleTags = sheet.getStyleTags();
    
    ctx.body = `
      <!DOCTYPE html>
      <html>
        <head>
          ${styleTags}
        </head>
        <body>
          <div id="root">${html}</div>
          <script src="/client.js"></script>
        </body>
      </html>
    `;
  } finally {
    sheet.seal();
  }
});

app.listen(3000);

四、样式提取 #

4.1 getStyleTags #

jsx
import { ServerStyleSheet } from 'styled-components';

const sheet = new ServerStyleSheet();

const styleTags = sheet.getStyleTags();

console.log(styleTags);

sheet.seal();

输出示例:

html
<style data-styled="true" data-styled-version="6.0.0">
  .sc-aXZVg{padding:12px 24px;}
  .sc-bZQynM{background:#667eea;color:white;}
</style>

4.2 getStyleElement #

jsx
import { ServerStyleSheet } from 'styled-components';

const sheet = new ServerStyleSheet();

const styleElements = sheet.getStyleElement();

console.log(styleElements);

sheet.seal();

4.3 收集所有样式 #

jsx
import { renderToString } from 'react-dom/server';
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';

function collectStyles(app) {
  const sheet = new ServerStyleSheet();
  
  try {
    const html = renderToString(
      <StyleSheetManager sheet={sheet.instance}>
        {app}
      </StyleSheetManager>
    );
    
    return {
      html,
      styles: sheet.getStyleTags(),
    };
  } finally {
    sheet.seal();
  }
}

const { html, styles } = collectStyles(<App />);

五、流式渲染 #

5.1 renderToNodeStream #

jsx
import { renderToNodeStream } from 'react-dom/server';
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';
import express from 'express';

const app = express();

app.get('*', (req, res) => {
  const sheet = new ServerStyleSheet();
  
  res.write(`
    <!DOCTYPE html>
    <html>
      <head>
        <title>My App</title>
  `);
  
  const stream = sheet.interleaveWithNodeStream(
    renderToNodeStream(
      <StyleSheetManager sheet={sheet.instance}>
        <App />
      </StyleSheetManager>
    )
  );
  
  stream.pipe(res, { end: false });
  
  stream.on('end', () => {
    res.write(`
      </head>
      <body>
        <script src="/client.js"></script>
      </body>
      </html>
    `);
    res.end();
    sheet.seal();
  });
});

5.2 renderToPipeableStream #

jsx
import { renderToPipeableStream } from 'react-dom/server';
import { ServerStyleSheet, StyleSheetManager } from 'styled-components';

function renderApp(res) {
  const sheet = new ServerStyleSheet();
  
  const { pipe } = renderToPipeableStream(
    <StyleSheetManager sheet={sheet.instance}>
      <App />
    </StyleSheetManager>,
    {
      onShellReady() {
        res.setHeader('Content-Type', 'text/html');
        pipe(res);
        sheet.seal();
      },
      onShellError(error) {
        res.status(500).send('Error');
        sheet.seal();
      },
    }
  );
}

六、客户端水合 #

6.1 hydrateRoot #

jsx
import { hydrateRoot } from 'react-dom/client';
import { StyleSheetManager } from 'styled-components';
import App from './App';

hydrateRoot(
  document.getElementById('root'),
  <StyleSheetManager>
    <App />
  </StyleSheetManager>
);

6.2 水合警告处理 #

jsx
import { hydrateRoot } from 'react-dom/client';
import { StyleSheetManager } from 'styled-components';

const sheet = new ServerStyleSheet();

hydrateRoot(
  document.getElementById('root'),
  <StyleSheetManager sheet={sheet.instance}>
    <App />
  </StyleSheetManager>,
  {
    onRecoverableError(error) {
      console.warn('Hydration error:', error);
    },
  }
);

七、性能优化 #

7.1 样式缓存 #

jsx
import { ServerStyleSheet } from 'styled-components';

const styleCache = new Map();

function getCachedStyles(cacheKey, renderFn) {
  if (styleCache.has(cacheKey)) {
    return styleCache.get(cacheKey);
  }
  
  const sheet = new ServerStyleSheet();
  
  try {
    const result = renderFn(sheet);
    styleCache.set(cacheKey, result);
    return result;
  } finally {
    sheet.seal();
  }
}

7.2 并行渲染 #

jsx
import { renderToString } from 'react-dom/server';
import { ServerStyleSheet } from 'styled-components';

async function renderParallel(components) {
  const results = await Promise.all(
    components.map(async (Component) => {
      const sheet = new ServerStyleSheet();
      
      try {
        const html = renderToString(
          <StyleSheetManager sheet={sheet.instance}>
            <Component />
          </StyleSheetManager>
        );
        
        return {
          html,
          styles: sheet.getStyleTags(),
        };
      } finally {
        sheet.seal();
      }
    })
  );
  
  return results;
}

7.3 样式去重 #

jsx
import { ServerStyleSheet } from 'styled-components';

function deduplicateStyles(styleTags) {
  const seen = new Set();
  const unique = [];
  
  const regex = /<style[^>]*>([\s\S]*?)<\/style>/g;
  let match;
  
  while ((match = regex.exec(styleTags)) !== null) {
    const content = match[1];
    if (!seen.has(content)) {
      seen.add(content);
      unique.push(match[0]);
    }
  }
  
  return unique.join('\n');
}

八、常见问题 #

8.1 样式闪烁 #

问题:页面加载时出现样式闪烁

解决方案:

jsx
import { ServerStyleSheet } from 'styled-components';

const sheet = new ServerStyleSheet();

const html = renderToString(
  <StyleSheetManager sheet={sheet.instance}>
    <App />
  </StyleSheetManager>
);

const styles = sheet.getStyleTags();

const document = `
  <!DOCTYPE html>
  <html>
    <head>
      ${styles}
    </head>
    <body>
      <div id="root">${html}</div>
    </body>
  </html>
`;

sheet.seal();

8.2 类名不匹配 #

问题:服务端和客户端类名不一致

解决方案:

javascript
const nextConfig = {
  compiler: {
    styledComponents: {
      ssr: true,
      displayName: true,
      preprocess: true,
    },
  },
};

8.3 内存泄漏 #

问题:ServerStyleSheet 未正确清理

解决方案:

jsx
const sheet = new ServerStyleSheet();

try {
  const html = renderToString(
    <StyleSheetManager sheet={sheet.instance}>
      <App />
    </StyleSheetManager>
  );
  
  return { html, styles: sheet.getStyleTags() };
} finally {
  sheet.seal();
}

九、调试技巧 #

9.1 样式检查 #

jsx
import { ServerStyleSheet } from 'styled-components';

const sheet = new ServerStyleSheet();

const html = renderToString(
  <StyleSheetManager sheet={sheet.instance}>
    <App />
  </StyleSheetManager>
);

console.log('Generated styles:', sheet.getStyleTags());
console.log('Generated HTML:', html);

sheet.seal();

9.2 样式统计 #

jsx
function analyzeStyles(styleTags) {
  const regex = /\.sc-[a-zA-Z0-9]+/g;
  const matches = styleTags.match(regex) || [];
  
  return {
    totalClasses: matches.length,
    uniqueClasses: new Set(matches).size,
  };
}

十、总结 #

SSR 要点速查表:

要点 说明
ServerStyleSheet 收集服务端样式
StyleSheetManager 管理样式表
getStyleTags 获取样式标签
getStyleElement 获取样式元素
seal 清理资源
hydrateRoot 客户端水合

下一步:学习 工具生态 掌握开发工具和插件。

最后更新:2026-03-28