并行SSR
前端实现 SSR(服务器端渲染)的并行加载,核心思想是避免服务器端的数据请求瀑布(Waterfall),将页面分块、流式(Streaming)地发送给浏览器,从而显著优化 Time To First Byte (TTFB) 和用户感知性能。
传统的 SSR 是一个"串行"过程:
- 服务器接收请求。
- 等待所有数据(API、数据库查询等)全部获取完毕。
- 将所有数据渲染成一个完整的 HTML 字符串。
- 将完整的 HTML 响应给浏览器。
这种模式的瓶颈在于第二步:如果任何一个数据请求很慢,整个页面都会被阻塞,用户会看到长时间的白屏。
而"并行加载"的 SSR,我们称之为 流式 SSR (Streaming SSR),其过程如下:
- 服务器接收请求。
- 立即发送页面的基本骨架(Shell),比如
<html>
,<head>
, 和不需要异步数据的静态部分。浏览器收到后可以立即开始解析和加载 CSS。 - 服务器并行地发起多个数据请求。
- 每当一个数据块准备好,服务器就将其渲染成 HTML 片段,并流式地推送到已经建立的连接中。
- 浏览器接收到这些 HTML 片段后,逐步将其渲染出来。通常,这些片段会带有内联的
<script>
标签,用于将 HTML 插入到正确的位置。
下面我们来探讨如何在主流框架中实现这一模式。
核心技术:React 的实现方式 (Suspense 和 React Server Components)
React 是流式 SSR 的引领者,主要通过 Suspense
和 React Server Components (RSC) 来实现。
1. 使用 Suspense
for SSR
Suspense
是实现流式渲染的关键。在服务器端,当 React 遇到一个 <Suspense>
组件时,它不会等待 Suspense
内部的异步操作完成。
工作流程:
- 发送 Fallback 内容 :服务器会先将
Suspense
的fallback
prop(例如一个加载中的 UI)作为占位符,连同页面的其他同步内容一起发送出去。 - 并行获取数据 :同时,服务器会继续处理
Suspense
内部组件的数据获取。 - 流式发送真实内容 :当内部组件的数据准备好并渲染完成后,React 会将这部分 HTML 片段,连同一个小型的内联
<script>
标签,作为一个新的"块"(Chunk)流式地发送到浏览器。 - 客户端激活:浏览器执行这个内联脚本,它会找到对应的 Fallback UI,并用新的 HTML 内容替换它。
代码示例 (Next.js / React 18+):
假设你有一个获取用户数据的组件 UserData
和一个获取文章列表的组件 PostList
,它们都很慢。
jsx
// components/SlowComponent.js
async function SlowComponent({ delay, children }) {
await new Promise(res => setTimeout(res, delay));
return <div>{children}</div>;
}
// app/page.js
import { Suspense } from 'react';
import SlowComponent from '../components/SlowComponent';
export default function Page() {
return (
<div>
<h1>我的主页</h1>
<p>这部分内容会立即显示。</p>
{/* 用户信息部分 */}
<Suspense fallback={<div>加载用户信息中...</div>}>
<SlowComponent delay={2000}>
<h2>用户信息</h2>
<p>用户名: 张三</p>
</SlowComponent>
</Suspense>
{/* 文章列表部分 */}
<Suspense fallback={<div>加载文章列表中...</div>}>
<SlowComponent delay={4000}>
<h2>文章列表</h2>
<ul>
<li>文章一</li>
<li>文章二</li>
</ul>
</SlowComponent>
</Suspense>
</div>
);
}
发生了什么?
- 服务器立即发送包含 "我的主页"、"这部分内容会立即显示"、"加载用户信息中..." 和 "加载文章列表中..." 的 HTML 骨架。用户几乎瞬间就能看到这个结构。
- 服务器同时 等待
SlowComponent
的两个实例。 - 2秒后,第一个
SlowComponent
完成。服务器流式发送用户信息部分的 HTML 和一个脚本,将其替换掉 "加载用户信息中..."。 - 再过2秒(总共4秒),第二个
SlowComponent
完成。服务器再次流式发送文章列表的 HTML 和脚本,将其替换掉 "加载文章列表中..."。
这样,用户不会等待4秒钟的白屏,而是在内容准备好时逐步看到它们。
2. React Server Components (RSC)
RSC 将并行数据获取提升到了一个新的层次。RSC 自身就可以是 async
函数,它在服务器上运行,并将渲染结果(一种特殊的 JSON 格式,不是 HTML)流式传输到客户端。
RSC 与 Suspense
结合使用,是目前最强大的并行加载模型。
工作流程:
- 根组件(通常是 Server Component)开始渲染。
- 它可以直接
await
数据获取,或者渲染其他 Server Components。 - 当它
await
一个异步操作时,渲染会暂停,但不会阻塞整个响应。 - React 可以将已渲染的部分先流式传输出去,并用
Suspense
包裹那些仍在等待数据的子树。
代码示例 (Next.js App Router):
jsx
// lib/api.js
export async function fetchUserData() {
await new Promise(res => setTimeout(res, 2000));
return { name: '张三' };
}
export async function fetchPosts() {
await new Promise(res => setTimeout(res, 4000));
return [{ id: 1, title: '文章一' }];
}
// components/UserData.js (Server Component)
import { fetchUserData } from '../lib/api';
export default async function UserData() {
const user = await fetchUserData(); // 直接在组件内 await
return (
<div>
<h2>用户信息</h2>
<p>用户名: {user.name}</p>
</div>
);
}
// components/PostList.js (Server Component)
import { fetchPosts } from '../lib/api';
export default async function PostList() {
const posts = await fetchPosts(); // 直接在组件内 await
return (
<div>
<h2>文章列表</h2>
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
</div>
);
}
// app/page.js (Server Component)
import { Suspense } from 'react';
import UserData from '../components/UserData';
import PostList from '../components/PostList';
export default function Page() {
return (
<div>
<h1>我的主页</h1>
{/* UserData 和 PostList 的数据获取是并行的! */}
<Suspense fallback={<div>加载用户信息...</div>}>
<UserData />
</Suspense>
<Suspense fallback={<div>加载文章列表...</div>}>
<PostList />
</Suspense>
</div>
);
}
在这个例子中,fetchUserData
和 fetchPosts
会并行执行 ,因为 React 会同时开始渲染 UserData
和 PostList
这两个子树。Suspense
提供了加载期间的 UI,让体验更加平滑。
Vue.js 的实现方式
Vue 3 也支持流式 SSR,其原理与 React 的 Suspense
非常相似。
核心 API:
renderToNodeStream()
(Node.js 环境) 或renderToWebStream()
(Web Streams API 环境)。<Suspense>
组件。
工作流程:
- 在服务器入口文件中,使用
renderToNodeStream
或renderToWebStream
来代替renderToString
。 - 在你的 Vue 组件中,使用
<Suspense>
来包裹需要异步加载数据的组件。 - 被包裹的组件可以在
setup
函数中返回一个 Promise (通过async setup
)。 - 服务器端渲染时,Vue 会先发送
<Suspense>
的#fallback
插槽内容。 - 当
async setup
的 Promise resolve 后,Vue 会将#default
插槽渲染的内容流式地发送到客户端,并附带脚本进行替换。
代码示例 (使用 Nuxt 3 或原生 Vue SSR):
vue
<!-- components/AsyncUser.vue -->
<template>
<div>
<h2>用户信息</h2>
<p>用户名: {{ user.name }}</p>
</div>
</template>
<script setup>
const user = await new Promise(res => {
setTimeout(() => {
res({ name: '李四' });
}, 2000);
});
</script>
vue
<!-- pages/index.vue -->
<template>
<div>
<h1>我的主页</h1>
<p>这部分内容立即显示。</p>
<Suspense>
<!-- 默认内容 -->
<template #default>
<AsyncUser />
</template>
<!-- 加载状态 -->
<template #fallback>
<div>加载用户信息中...</div>
</template>
</Suspense>
</div>
</template>
<script setup>
import AsyncUser from '~/components/AsyncUser.vue';
</script>
这个 Vue 示例的行为和 React 的 Suspense
示例几乎完全一样,实现了同样的目标。
其他架构和思想
-
Islands Architecture (群岛架构)
- 像 Astro 这样的框架是这种架构的典型代表。
- 它默认输出零 JavaScript 的纯 HTML。页面上的交互部分被视为"岛屿"(Island),只有当这些岛屿进入视口或被用户交互时,才会加载它们的 JavaScript。
- 在 SSR 阶段,Astro 可以在构建时或请求时并行获取不同组件的数据,然后生成一个高度优化的静态 HTML 页面。这本身就是一种并行化的体现。
-
Edge-Side Rendering (边缘渲染)
- 将 SSR 的逻辑部署到 CDN 的边缘节点(如 Vercel, Netlify, Cloudflare Workers)。
- 虽然这不直接改变代码的并行逻辑,但它极大地减少了用户到服务器的物理延迟。
- 当你的 SSR 服务器离数据源(数据库/API)很近时,在边缘并行获取数据可以获得极速的响应。
总结
实现前端 SSR 的并行加载,关键在于从"一次性完整响应"转向"分块流式响应"。
策略 | 核心技术 | 优点 | 框架支持 |
---|---|---|---|
流式渲染 | Suspense 组件 |
逐步展现内容,改善用户感知性能,不阻塞首屏静态内容 | React 18+ , Vue 3+ |
组件级数据获取 | React Server Components (async /await in components) |
将数据获取逻辑内聚到组件中,代码更清晰,自动实现并行数据拉取 | React (Next.js App Router) |
架构模式 | Islands Architecture | 默认静态化,按需加载交互,性能极佳 | Astro , Fresh |
部署优化 | Edge Computing | 减少网络延迟,让并行获取更快 | Vercel, Netlify, Cloudflare 等平台 |
对于现代前端开发,强烈推荐使用支持流式 SSR 的框架(如 Next.js 13+ 或 Nuxt 3) ,并充分利用 Suspense
和异步组件,这是实现高性能 SSR 并行加载的最直接、最强大的方式。