> For AI agents: the complete documentation index is available at /zh/llms.txt, the full documentation bundle is available at /zh/llms-full.txt.

# 服务端渲染（SSR）

服务端渲染（Server-Side Rendering，简称 SSR）在服务器端生成完整的 HTML 页面，然后发送到浏览器端直接显示，无需客户端额外渲染。

## 核心优势

- **更快首屏**：服务端预渲染，浏览器直接显示，无需等待 JavaScript 执行
- **更好 SEO**：搜索引擎可直接索引完整的 HTML 内容
- **开箱即用**：无需编写复杂服务端逻辑，无需单独运维

:::info 默认渲染模式
Modern.js 的 SSR **默认使用流式渲染（Streaming SSR）**，页面会边渲染边返回，用户可以更快看到初始内容。

详细用法请参考[流式服务端渲染（Streaming SSR）](/zh/guides/basic-features/render/streaming-ssr.md)文档。

如需切换到传统 SSR 模式（等待所有数据加载完成后一次性返回），可配置：

```ts title="modern.config.ts"
import { defineConfig } from '@modern-js/app-tools';

export default defineConfig({
  server: {
    ssr: {
      mode: 'string', // 传统 SSR 模式
    },
  },
});
```

:::

## 开启 SSR

在 Modern.js 启用 SSR 非常简单，只需要设置 [`server.ssr`](/zh/configure/app/server/ssr.md) 为 `true` 即可：

```ts title="modern.config.ts"
import { defineConfig } from '@modern-js/app-tools';

export default defineConfig({
  server: {
    ssr: true, // 默认启用流式渲染
  },
});
```

## 数据获取

Modern.js 提供了 Data Loader，支持在 SSR 和 CSR 下同构地获取数据。每个路由模块（如 `layout.tsx` 和 `page.tsx`）都可以定义自己的 Data Loader：

:::tip 了解更多
以下方式不会有流式渲染效果，要想获得流式渲染效果请参考[流式服务端渲染（Streaming SSR）](/zh/guides/basic-features/render/streaming-ssr.md)

:::

```ts title="src/routes/page.data.ts"
export const loader = () => {
  return {
    message: 'Hello World',
  };
};
```

在组件中通过 Hooks API 获取数据：

```tsx
import { useLoaderData } from '@modern-js/runtime/router';
export default () => {
  const data = useLoaderData();
  return <div>{data.message}</div>;
};
```

### 使用 Client Loader

默认情况下，在 SSR 应用中，`loader` 函数只会在服务端执行。但有些场景下，开发者可能期望在浏览器端发送的请求不经过 SSR 服务，直接请求数据源，例如：

1. 在浏览器端希望减少网络消耗，直接请求数据源。
2. 应用在浏览器端有数据缓存，不希望请求 SSR 服务获取数据。

Modern.js 支持在 SSR 应用中额外添加 `.data.client` 文件，同样具名导出 `loader`。此时 SSR 应用在服务端执行 Data Loader 报错降级，或浏览器端切换路由时，会像 CSR 应用一样在浏览器端执行该 `loader` 函数，而不是再向 SSR 服务发送数据请求。

```ts title="page.data.client.ts"
import cache from 'my-cache';

export async function loader({ params }) {
  if (cache.has(params.id)) {
    return cache.get(params.id);
  }
  const res = await fetch(`URL_ADDRESS?id=${params.id}`);
  const data = await res.json();
  return {
    message: data.message,
  };
}
```

## SSR 降级

在 Modern.js 中，如果应用在 SSR 过程中出现异常，Modern.js 会自动降级到 CSR 模式，并在 CSR 重新发起数据请求，保证页面能够正常展示。SSR 降级的原因主要分为两种：

1. Data Loader 执行报错
2. React 组件在服务端渲染报错

### Data Loader 执行报错

默认情况下，如果路由对应的 `loader` 函数执行报错，框架会在服务端直接渲染 `<ErrorBoundary>` 组件，并展示错误信息，这也是社区内大多数框架的默认行为。

Modern.js 也支持通过 [`server.ssr`](/zh/configure/app/server/ssr.md) 配置项中的 `loaderFailureMode` 字段，自定义降级策略。当该字段被配置为 `clientRender` 时，会直接降级到 CSR 模式，并重新发起数据请求。

此时，如果路由中定义了 Client Loader，则会优先使用 Client Loader 发起数据请求。如果重新渲染仍然出错，再展示 `<ErrorBoundary>` 组件。


### 组件渲染报错

如果 Data Loader 执行正常，但组件渲染报错时，SSR 渲染将会部分或完全失败，例如以下代码：

```tsx
import { Await, useLoaderData } from '@modern-js/runtime/router';
import { Suspense } from 'react';

const Page = () => {
  const data = useLoaderData();
  const isNode = typeof window === 'undefined';
  const undefinedVars = data.unDefined;
  const definedVars = data.defined;

  return <div>{isNode ? undefinedVars.msg : definedVars.msg}</div>;
};

export default Page;
```

此时，Modern.js 会将页面降级为 CSR，并利用 Data Loader 中已有的数据渲染。如果重新渲染仍然出错，则展示 `<ErrorBoundary>` 组件。

:::tip
组件渲染报错的行为，不会受到 `loaderFailureMode` 的影响，也不会在浏览器端执行 Client Loader。
:::


## 日志与监控

:::tip
SSR 相关的数据监控已整合到 Monitors 模块中，详见 [Monitors](/zh/guides/advanced-features/server-monitor/monitors.md)。
:::

## 页面缓存

Modern.js 中内置了缓存的能力，详细请参考[渲染缓存](/zh/guides/basic-features/render/ssr-cache.md)。

## 运行环境差异

SSR 应用会同时运行在服务端和浏览器端，两者在运行环境上不完全相同，存在 Web API 和 Node API 的差异。

开启 SSR 时，Modern.js 会用相同的入口，构建出 SSR Bundle 和 CSR Bundle 两份产物。因此，在 SSR Bundle 中存在 Web API，或是在 CSR Bundle 中存在 Node API 时，都可能导致运行出错。出现这类问题的场景主要是两类：

- 应用自身代码存在问题
- 应用依赖的包中存在副作用

### 自身代码问题

这种场景通常出现在应用从 CSR 迁移到 SSR，CSR 应用通常会在代码中引入 Web API。例如应用希望做全局的事件监听：

```tsx
document.addEventListener('load', () => {
  console.log('document load');
});
const App = () => {
  return <div>Hello World</div>;
};
export default App;
```

对于这种场景，你可以直接使用 Modern.js 内置的环境变量 `MODERN_TARGET` 进行判断，在构建时删除无用代码：

```ts
if (process.env.MODERN_TARGET === 'browser') {
  document.addEventListener('load', () => {
    console.log('document load');
  });
}
```

开发环境打包后，SSR 产物和 CSR 产物会被编译成以下内容。因此 SSR 环境中不会再因为 Web API 报错：

```ts
// SSR 产物
if (false) {
}

// CSR 产物
if (true) {
  document.addEventListener('load', () => {
    console.log('document load');
  });
}
```

:::note
更多内容可以查看[环境变量](/zh/guides/basic-features/env-vars.md)。
:::

### 依赖中的副作用

这类场景是在 SSR 应用中随时可能出现的，因为社区中的包并不都支持在两个运行环境中运行，有些包也无需在两个环境中运行。例如在代码中引入了包 A，它内部有使用了 Web API 的副作用：

```ts title="packageA"
document.addEventListener('load', () => {
  console.log('document load');
});

export const doSomething = () => {};
```

如果直接引用到组件中，会造成 SSR 加载报错，即使你已经使用环境变量进行判断，但仍然无法移除依赖中副作用的执行。

```tsx title="routes/page.tsx"
import { doSomething } from 'packageA';

export const Page = () => {
  if (process.env.MODERN_TARGET === 'browser') {
    doSomething();
  }
  return <div>Hello World</div>;
};
```

Modern.js 也支持通过 `.server.` 后缀的文件来区分 SSR Bundle 和 CSR Bundle 产物的打包文件。可以创建同名的 `.ts` 和 `.server.ts` 文件做一层代理：

```ts title="a.ts"
export { doSomething } from 'packageA';
```

```ts title="a.server.ts"
export const doSomething: any = () => {};
```

在文件中直接引入 `./a`，此时 SSR 打包下会优先使用 `.server.ts` 后缀的文件，CSR 打包下会使用 `.ts` 后缀的文件。

```tsx title="routes/page.tsx"
import { doSomething } from './a';

export const Page = () => {
  doSomething();
  return <div>Hello World</div>;
};
```

## 常见问题

### 保持渲染一致

SSR 业务需要保证在服务端渲染时的结果和浏览器端 Hydrate 的结果一致，否则很有可能出现不符合预期的渲染结果。这里通过一个例子，演示当 SSR 与 CSR 渲染不一致时出现的问题，在组件中添加以下代码：

```tsx
{
  typeof window !== 'undefined' ? <div>browser content</div> : null;
}
```

启动应用后，访问页面，会发现浏览器控制台抛出警告信息：

```sh
Warning: Expected server HTML to contain a matching <div> in <div>.
```

这是 React hydrate 结果与 SSR 渲染结果不一致造成的。虽然当前页面表现正常，但在复杂应用中，很有可能因此出现 DOM 层级混乱、样式混乱等问题。

:::info
关于 React hydrate 逻辑请参考[这里](https://zh-hans.react.dev/reference/react-dom/hydrate)。

:::

应用需要保持 SSR 与 CSR 渲染结果的一致性，如果存在不一致的情况，说明这部分内容无需在 SSR 中进行渲染。Modern.js 为这类在 SSR 中不需要渲染的内容提供 [`<NoSSR>` 工具组件](/zh/apis/app/runtime/ssr/no-ssr.md)：

```ts
import { NoSSR } from '@modern-js/runtime/ssr';
```

在不需要进行 SSR 的元素外部，用 `NoSSR` 组件包裹：

```tsx
<NoSSR>
  <div>browser content</div>
</NoSSR>
```

修改代码后，刷新页发现之前的 Waring 消失。打开浏览器开发者工具的 Network 窗口，查看返回的 HTML 文档是不包含 `NoSSR` 组件包裹的内容的。

在实际场景中，有些应用的 UI 展示会和用户设备有关，例如 [UA](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) 信息。Modern.js 也提供了 `use(RuntimeContext)` 这类 API，可以在组件中获取完整的请求信息，利用它保证 SSR 与 CSR 的渲染结果一致。详细用法请参考[运行时上下文](/zh/guides/basic-features/render/before-render.md#运行时上下文)。

### 关注内存泄漏

:::warning 警告
在 SSR 场景下，开发者需要特别关注内存泄露问题，即使是微小的内存泄露，在大量的访问后也会对服务造成影响。

:::

SSR 时，浏览器的每次请求，都会触发服务端重新执行一次组件渲染逻辑。所以，需要避免在全局定义任何可能不断增长的数据结构，或在全局进行事件订阅，或创建不会被销毁的流。

例如以下代码，使用 [redux-observable](https://redux-observable.js.org/) 时，习惯了 CSR 的开发者通常会在组件中这样编码：

```tsx
/* 代码仅作为示例，不可运行 */
import { createEpicMiddleware, combineEpics } from 'redux-observable';

const epicMiddleware = createEpicMiddleware();
const rootEpic = combineEpics();

export default function Test() {
  epicMiddleware.run(rootEpic);
  return <div>Hello Modern.js</div>;
}
```

在组件外层创建 Middleware 实例 `epicMiddleware`，并在组件内部调用 `epicMiddleware.run`。

在浏览器端，这段代码不会造成任何问题，但是在 SSR 时，Middleware 实例会一直无法被销毁。每次渲染组件，调用 `epicMiddleware.run(rootEpic)` 时，都会在内部添加新的事件绑定，导致整个对象不断变大，最终对应用性能造成影响。

CSR 中这类问题不易被发觉，因此从 CSR 切换到 SSR 时，如果不确定应用是否存在这类隐患，可以对应用进行压测。
