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

# 流式服务端渲染（Streaming SSR）

流式渲染是一种先进的渲染方式，它可以在页面渲染过程中逐步返回内容，从而显著提升用户体验。

在传统的 SSR 渲染方式中，页面的渲染是一次性完成的，需要等待所有数据加载完成后才能返回完整的 HTML。而在流式渲染中，页面的渲染是逐步完成的，可以边渲染边返回，用户能够更快地看到初始内容。

:::info 默认模式
**Streaming SSR 是 Modern.js SSR 的默认渲染模式**。当你启用 SSR 时，无需额外配置即可使用流式渲染。

如果你需要切换到传统的 SSR 模式（等待所有数据加载完成后一次性返回），可以配置 `server.ssr.mode` 为 `'string'`。详细说明请参考[服务端渲染（SSR）](/zh/guides/basic-features/render/ssr.md)文档。
:::

相比传统 SSR 渲染：

- **更快感知速度**：流式渲染可以在渲染过程中逐步显示内容，能够以最快的速度显示业务首页
- **更好的用户体验**：通过流式渲染，用户可以更快地看到页面上的内容，而不需要等待整个页面都渲染完成后才能交互
- **更好的性能控制**：流式渲染可以让开发者更好地控制页面加载的优先级和顺序，从而更好地优化性能和用户体验
- **更好的适应性**：流式渲染可以更好地适应不同网络速度和设备性能，使得页面在各种环境下都能有更好的表现

## 开启流式渲染

Modern.js 支持了 React 18+ 的流式渲染。当你启用 SSR 时，流式渲染默认开启：

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

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

如果你需要显式指定流式渲染模式，可以配置：

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

export default defineConfig({
  server: {
    ssr: {
      mode: 'stream', // 显式指定流式渲染模式
    },
  },
});
```

Modern.js 的流式渲染基于 React Router 实现，主要涉及 API 有：

- [`Await`](https://reactrouter.com/en/main/components/await)：用于渲染 Data Loader 返回的异步数据。
- [`useAsyncValue`](https://reactrouter.com/en/main/hooks/use-async-value)：用于从最近的父级 `Await` 组件中获取数据。

## 获取数据

```ts title="user/[id]/page.data.ts"
import { defer, type LoaderFunctionArgs } from '@modern-js/runtime/router';

interface User {
  name: string;
  age: number;
}

export interface Data {
  data: User;
}

export const loader = ({ params }: LoaderFunctionArgs) => {
  const userId = params.id;

  const user = new Promise<User>(resolve => {
    setTimeout(() => {
      resolve({
        name: `user-${userId}`,
        age: 18,
      });
    }, 200);
  });

  return defer({ data: user });
};
```

`user` 是一个 Promise 类型的对象，表示需要异步获取的数据，通过 `defer` 处理需要异步获取的 `user`。注意，`defer` 必须接收一个对象类型的参数，不能直接传递 Promise 对象。

另外，`defer` 还可以同时接收异步数据和同步数据。在下述例子中，我们等待部分耗时较短的请求，在响应后通过对象数据返回，而耗时较长时间的请求，则通过 Promise 返回：

```ts title="user/[id]/page.data.ts"
export const loader = async ({ params }: LoaderFunctionArgs) => {
  const userId = params.id;

  const user = new Promise<User>(resolve => {
    setTimeout(() => {
      resolve({
        name: `user-${userId}`,
        age: 18,
      });
    }, 2000);
  });

  const otherData = new Promise<string>(resolve => {
    setTimeout(() => {
      resolve('some sync data');
    }, 200);
  });

  return defer({
    data: user,
    other: await otherData,
  });
};
```

这样，应用无需等待最耗时的数据请求响应后才展示页面内容，可以优先展示部分有数据的页面内容

## 渲染数据

通过 `Await` 组件，可以获取到 Data Loader 中异步返回的数据，然后进行渲染。例如：

```tsx title="user/[id]/page.tsx"
import { Await, useLoaderData } from '@modern-js/runtime/router';
import { Suspense } from 'react';
import type { Data } from './page.data';

const Page = () => {
  const data = useLoaderData() as Data;

  return (
    <div>
      User info:
      <Suspense fallback={<div id="loading">loading user data ...</div>}>
        <Await resolve={data.data}>
          {user => {
            return (
              <div id="data">
                name: {user.name}, age: {user.age}
              </div>
            );
          }}
        </Await>
      </Suspense>
    </div>
  );
};

export default Page;
```

`Await` 需要包裹在 `Suspense` 组件内部，`Await` 的 `resolve` 传入的是 Data Loader 异步获取的数据，当数据获取完成后，
通过 [Render Props](https://zh-hans.react.dev/reference/react/cloneElement#passing-data-with-a-render-prop) 模式，渲染获取到的数据。在数据的获取阶段，将展示
`Suspense` 组件 `fallback` 属性设置的内容。

:::warning 注意
从 `page.data.ts` 文件导入类型时，需要使用 `import type` 语法，保证只导入类型信息，避免 Data Loader 的代码打包到前端产物中。
:::

在组件中，你也可以通过 `useAsyncValue` 获取 Data Loader 返回的异步数据。例如：

```tsx title='page.tsx'
import { useAsyncValue } from '@modern-js/runtime/router';

const UserInfo = () => {
  const user = useAsyncValue();
  return (
    <div>
      name: {user.name}, age: {user.age}
    </div>
  );
};

const Page = () => {
  const data = useLoaderData() as Data;
  return (
    <div>
      User info:
      <Suspense fallback={<div id="loading">loading user data ...</div>}>
        <Await resolve={data.data}>
          <UserInfo />
        </Await>
      </Suspense>
    </div>
  );
};

export default Page;
```

## 错误处理

`Await` 组件的 `errorElement` 属性，可以用来处理 Data Loader 或者子组件渲染时的报错。例如，我们故意在 Data Loader 函数中抛出错误：

```ts title="page.loader.ts"
import { defer } from '@modern-js/runtime/router';

export default () => {
  const data = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error('error occurs'));
    }, 200);
  });

  return defer({ data });
};
```

然后通过 `useAsyncError` 获取错误，并将用于渲染错误信息的组件赋值给 `Await` 组件的 `errorElement` 属性：

```tsx title="page.ts"
import { Await, useAsyncError, useLoaderData } from '@modern-js/runtime/router';
import { Suspense } from 'react';

export default function Page() {
  const data = useLoaderData();

  return (
    <div>
      Error page
      <Suspense fallback={<div>loading ...</div>}>
        <Await resolve={data.data} errorElement={<ErrorElement />}>
          {(data: any) => {
            return <div>never displayed</div>;
          }}
        </Await>
      </Suspense>
    </div>
  );
}

function ErrorElement() {
  const error = useAsyncError() as Error;
  return <p>Something went wrong! {error.message}</p>;
}
```

## 控制是否等待全部内容再输出

流式传输可以提高用户体验，因为当页面内容可用时，用户可以及时感知到它们。但在部分场景下（例如 SEO 爬虫、特定 AB 实验或合规页面）希望等所有内容完成后再一次性输出。

Modern.js 默认行为的判定优先级为：

Modern.js 使用 [isbot](https://www.npmjs.com/package/isbot) 对请求的 `user-agent`，以判断请求是否来自爬虫。

1. 请求头 `x-should-stream-all`（中间件可写）。
2. 环境变量 `MODERN_JS_STREAM_TO_STRING`（强制全量）。
3. [isbot](https://www.npmjs.com/package/isbot) 检测 `user-agent`（爬虫全量）。
4. 默认流式（先 shell 后内容）。

你可以在自定义中间件里按请求动态写入标记，控制是否等待全部内容：

```ts title="middleware 示例"
export const middleware = async (ctx, next) => {
  const ua = ctx.req.header('user-agent') || '';
  const shouldWaitAll =
    /Lighthouse|Googlebot/i.test(ua) || ctx.req.path === '/marketing';

  // 写入布尔值字符串，true 表示使用 onAllReady，false 表示使用 onShellReady
  ctx.req.headers.set('x-should-stream-all', String(shouldWaitAll));

  await next();
};
```


## 相关文档

- [渲染模式总览](/zh/guides/basic-features/render/overview.md)
- [服务端渲染（SSR）](/zh/guides/basic-features/render/ssr.md)
- [渲染缓存](/zh/guides/basic-features/render/ssr-cache.md)
- [React Server Components（RSC）](/zh/guides/basic-features/render/rsc.md) - 与 Streaming SSR 结合使用
- [New Suspense SSR Architecture in React 18](https://github.com/reactwg/react-18/discussions/37) - React 18 架构说明
