SSR、CSR,这两个词对于前端开发者来说早已耳熟能详。它们的本质区别究竟在哪里?简单来说:
- SSR 由服务端直接生成并返回包含首屏内容的 HTML。
- CSR 由服务端返回一个空的根节点(如
root),浏览器需要先解析 JavaScript 再动态填充内容。
这其中的关键差异导致了渲染时间的不同:SSR 无需等待浏览器解析和执行 JS,在一次 HTTP 请求中即可获得可渲染的 HTML。
那么,业界那些优秀的、支持 SSR 的开源框架是如何实现这一机制的呢?本文将以阿里巴巴开源的 ice.js 框架为例,进行源码级的剖析,带你理解 SSR 的实现原理,以及与 CSR 的异同。
一、SSR 的核心实现
SSR 必不可少的一个环节,就是需要一台能够 “解析 SSR 脚本” 的服务器。相比之下,CSR 和 SSG 只需要静态托管即可——我们每次构建后生成的 HTML 文件是固定的,直接放在托管站点供访问就行。
那么,在 ice.js 中,解析 SSR 脚本 这件事具体做了哪些工作呢?我们可以先进行一个大致的构思:
- 应该会有一个
express 或 koa 服务,用于接收所有请求,并根据请求的路由返回对应的组件。
- 应该包含一些处理 服务端 React 组件 的代码,很可能会基于
react-dom/server。
- 最终会以 HTTP 响应或流式渲染的形式将结果返回给前端。
这大概是我们能想到的主要部分。现在,让我们直接去源码中寻找答案。
这些能力本质上属于 运行时(runtime) 范畴。在 /runtime/runServerApp.tsx 中,我找到了处理服务端渲染的核心部分:

一个名为 renderToResponse 的函数直接映入眼帘,顾名思义,它就是负责“渲染并响应”的核心函数。
export async function renderToResponse(requestContext: ServerContext, renderOptions: RenderOptions) {
const { res } = requestContext;
const result = await doRender(requestContext, renderOptions);
const { value } = result;
if (typeof value === 'string') {
sendResult(res, result);
} else {
const { pipe, fallback } = value;
res.statusCode = 200;
res.setHeader('Content-Type', 'text/html; charset=utf-8');
try {
await pipeToResponse(res, pipe);
} catch (error) {
if (renderOptions.disableFallback) {
throw error;
}
console.error('PiperToResponse error, downgrade to CSR.', error);
// downgrade to CSR.
const result = await fallback();
sendResult(res, result);
}
}
}
这个函数的核心逻辑非常清晰:
- 获取网络请求的
response 对象。
- 调用
doRender 进行 前端组件到 HTML 的转换。
- 根据结果类型,直接进行 HTTP 响应或流式响应。
- 如果渲染过程出现异常,则 降级到 CSR 渲染,返回空节点。
至此,我们之前的几个猜想都得到了印证。接下来,让我们深入看一下 doRender 函数是如何工作的。
async function doRender(serverContext: ServerContext, renderOptions: RenderOptions): Promise<RenderResult> {
const { req } = serverContext;
const {
app,
basename,
serverOnlyBasename,
routes,
documentOnly,
disableFallback,
assetsManifest,
runtimeModules,
renderMode,
runtimeOptions,
} = renderOptions;
const location = getLocation(req.url);
const requestContext = getRequestContext(location, serverContext);
const appConfig = getAppConfig(app);
let appData: any;
const appContext: AppContext = {
appExport: app,
routes,
appConfig,
appData,
routesData: null,
routesConfig: null,
assetsManifest,
basename,
matches: [],
};
const runtime = new Runtime(appContext, runtimeOptions);
runtime.setAppRouter(DefaultAppRouter);
// Load static module before getAppData.
if (runtimeModules.statics) {
await Promise.all(runtimeModules.statics.map(m => runtime.loadModule(m)).filter(Boolean));
}
// don‘t need to execute getAppData in CSR
if (!documentOnly) {
try {
appData = await getAppData(app, requestContext);
} catch (err) {
console.error(‘Error: get app data error when SSR.‘, err);
}
}
// HashRouter loads route modules by the CSR.
if (appConfig?.router?.type === ’hash‘) {
return renderDocument({ matches: [], renderOptions });
}
const matches = matchRoutes(routes, location, serverOnlyBasename || basename);
if (!matches.length) {
return render404();
}
const routePath = getCurrentRoutePath(matches);
if (documentOnly) {
return renderDocument({ matches, routePath, renderOptions });
}
try {
const routeModules = await loadRouteModules(matches.map(({ route: { id, load } }) => ({ id, load })));
const routesData = await loadRoutesData(matches, requestContext, routeModules, renderMode);
const routesConfig = getRoutesConfig(matches, routesData, routeModules);
runtime.setAppContext({ ...appContext, routeModules, routesData, routesConfig, routePath, matches, appData });
if (runtimeModules.commons) {
await Promise.all(runtimeModules.commons.map(m => runtime.loadModule(m)).filter(Boolean));
}
return await renderServerEntry({
runtime,
matches,
location,
renderOptions,
});
} catch (err) {
if (disableFallback) {
throw err;
}
console.error(‘Warning: render server entry error, downgrade to csr.‘, err);
return renderDocument({ matches, routePath, renderOptions, downgrade: true });
}
}
doRender 函数主要完成了以下几件关键事情:
-
解析请求与基础配置
const location = getLocation(req.url);
const requestContext = getRequestContext(location, serverContext);
const appConfig = getAppConfig(app);
-
初始化应用上下文与运行时上下文(框架层面的设计)
const appContext: AppContext = {
appExport: app,
routes,
appConfig,
appData,
routesData: null,
routesConfig: null,
assetsManifest,
basename,
matches: [],
};
const runtime = new Runtime(appContext, runtimeOptions);
-
Hash 路由模式直接返回 HTML(因为不支持 SSR)
if (appConfig?.router?.type === ’hash‘) {
return renderDocument({ matches: [], renderOptions });
}
-
进行路由匹配,支持默认 404
const matches = matchRoutes(routes, location, serverOnlyBasename || basename);
if (!matches.length) {
return render404();
}
-
执行真正的 SSR 渲染
return await renderServerEntry({
runtime,
matches,
location,
renderOptions,
});
这里实际上还处理了大量框架层面的 “渲染前准备工作”,例如初始化运行时、记录 App 全局状态等,主要用于提供框架对外暴露的生命周期钩子函数。这些与 SSR 的核心渲染逻辑关系不大,我们暂且略过。
那么,最关键的一步 renderServerEntry 是如何执行渲染的呢?
/**
* Render App by SSR.
*/
async function renderServerEntry(
{
runtime,
matches,
location,
renderOptions,
}: RenderServerEntry,
): Promise<RenderResult> {
const { Document } = renderOptions;
const appContext = runtime.getAppContext();
const { appData, routePath } = appContext;
const staticNavigator = createStaticNavigator();
const AppRuntimeProvider = runtime.composeAppProvider() || React.Fragment;
const RouteWrappers = runtime.getWrappers();
const AppRouter = runtime.getAppRouter();
const documentContext = {
main: <App
action={Action.Pop}
location={location}
navigator={staticNavigator}
static
RouteWrappers={RouteWrappers}
AppRouter={AppRouter}
/>,
};
const element = (
<AppDataProvider value={appData}>
<AppRuntimeProvider>
<AppContextProvider value={appContext}>
<DocumentContextProvider value={documentContext}>
<Document pagePath={routePath} />
</DocumentContextProvider>
</AppContextProvider>
</AppRuntimeProvider>
</AppDataProvider>
);
const pipe = renderToNodeStream(element, false);
const fallback = () => {
return renderDocument({ matches, routePath, renderOptions, downgrade: true });
};
return {
value: {
pipe,
fallback,
},
};
}
renderServerEntry 函数是 SSR 核心链路的最后一步,直接触达用户响应。它主要完成了以下几件事:
-
聚合上下文:将 doRender 初始化和准备的 runtime、AppData、AppContext 等全局上下文,全部注入到 React 应用的根 Provider 中,以便业务组件消费。
const { Document } = renderOptions;
const appContext = runtime.getAppContext();
const { appData, routePath } = appContext;
const staticNavigator = createStaticNavigator();
const AppRuntimeProvider = runtime.composeAppProvider() || React.Fragment;
const RouteWrappers = runtime.getWrappers();
const AppRouter = runtime.getAppRouter();
-
准备根节点 App:创建包含基础路由能力、严格模式、全局异常捕获等功能的根组件。
const documentContext = {
main: <App
action={Action.Pop}
location={location}
navigator={staticNavigator}
static
RouteWrappers={RouteWrappers}
AppRouter={AppRouter}
/>,
};
-
组装完整的 React 元素树
const element = (
<AppDataProvider value={appData}>
<AppRuntimeProvider>
<AppContextProvider value={appContext}>
<DocumentContextProvider value={documentContext}>
<Document pagePath={routePath} />
</DocumentContextProvider>
</AppContextProvider>
</AppRuntimeProvider>
</AppDataProvider>
);
-
执行流式渲染(基于 react-dom/server 的 renderToPipeableStream API)
const pipe = renderToNodeStream(element, false);
让我们看看 renderToNodeStream 的源码实现:
import * as Stream from ’stream‘;
import type * as StreamType from ’stream‘;
import * as ReactDOMServer from ’react-dom/server‘;
const { Writable } = Stream;
export type NodeWritablePiper = (
res: StreamType.Writable,
next?: (err?: Error) => void
) => void;
export function renderToNodeStream(
element: React.ReactElement,
generateStaticHTML: boolean,
): NodeWritablePiper {
return (res, next) => {
const { pipe } = ReactDOMServer.renderToPipeableStream(
element,
{
onShellReady() {
if (!generateStaticHTML) {
pipe(res);
}
},
onAllReady() {
if (generateStaticHTML) {
pipe(res);
}
next();
},
onError(error: Error) {
next(error);
},
},
);
};
}
至此,渲染结果返回到入口函数 renderToResponse,一次完整的服务端渲染流程便结束了。
/**
* Render and send the result to ServerResponse.
*/
export async function renderToResponse(
requestContext: ServerContext, renderOptions: RenderOptions
) {
const { res } = requestContext;
const result = await doRender(requestContext, renderOptions);
const { value } = result;
if (typeof value === ’string‘) {
sendResult(res, result);
} else {
const { pipe, fallback } = value;
res.statusCode = 200;
res.setHeader(’Content-Type‘, ’text/html; charset=utf-8‘);
try {
await pipeToResponse(res, pipe);
} catch (error) {
if (renderOptions.disableFallback) {
throw error;
}
console.error(’PiperToResponse error, downgrade to CSR.‘, error);
// downgrade to CSR.
const result = await fallback();
sendResult(res, result);
}
}
}
二、开发阶段如何实现?
另一个巧妙之处在于开发阶段。我们都知道 SSR 依赖于服务端,那么在开发阶段,ice.js 是如何基于 webpack dev server 来实现 SSR 的呢?
锁定到 /ice/src/commands 目录,这里包含了框架的各种命令行逻辑。找到 start 命令相关的代码:

秘密就在这里!框架巧妙地借用了 dev server 的中间件机制,在开发服务器中拦截请求,直接触发 SSR 渲染链路,而无需启动一个独立的后端服务。
我们来细看一下 createRenderMiddleware 这个核心中间件函数:
export default function createRenderMiddleware(options: Options): Middleware {
const {
documentOnly,
renderMode,
serverCompileTask,
routeManifestPath,
getAppConfig,
taskConfig,
userConfig,
} = options;
const middleware: ExpressRequestHandler = async function (req, res, next) {
const routes = JSON.parse(fse.readFileSync(routeManifestPath, ’utf-8‘));
const appConfig = (await getAppConfig()).default;
if (appConfig?.router?.type === ’hash‘) {
warnOnHashRouterEnabled(userConfig);
}
const basename = getRouterBasename(taskConfig, appConfig);
const matches = matchRoutes(routes, req.path, basename);
// When documentOnly is true, it means that the app is CSR and it should return the html.
if (matches.length || documentOnly) {
// Wait for the server compilation to finish
const { serverEntry, error } = await serverCompileTask.get();
if (error) {
consola.error(’Server compile error in render middleware.‘);
return;
}
let serverModule;
try {
delete require.cache[serverEntry];
serverModule = await dynamicImport(serverEntry, true);
} catch (err) {
// make error clearly, notice typeof err === ’string‘
consola.error(`import ${serverEntry} error: ${err}`);
return;
}
const requestContext: ServerContext = {
req,
res,
};
serverModule.renderToResponse(requestContext, {
renderMode,
documentOnly,
});
} else {
next();
}
};
return {
name: ’server-render‘,
middleware,
};
}
这个中间件的核心逻辑与生产环境的 SSR 主渲染函数并无本质区别。其巧妙之处在于:它基于 appConfig 配置(判断项目是否启用了 SSR 模式),在 webpack dev server 的处理链路中插入了一层拦截。
如果是 SSR 模式,则动态引入服务端入口模块,直接走上面分析过的 renderToResponse 渲染链路;如果不是 SSR 模式,则放行请求,走常规的 webpack 构建与静态资源服务链路。这种设计保证了开发体验与生产行为的一致性,同时也充分利用了现有构建工具链。
三、总结
通过深入 ice.js 的源码,我们可以清晰地看到一个现代 React SSR 框架的核心实现路径:从接收 HTTP 请求、匹配路由、准备数据、组装 React 组件树,到最终利用 react-dom/server 进行流式渲染或字符串渲染,并妥善处理降级逻辑。
同时,其在开发阶段通过中间件巧妙集成 webpack dev server 的设计,也展示了框架如何平衡开发效率与功能完整性。这种架构模式在 Node.js 运行时上提供了强大的服务端渲染能力,是现代前端框架实现 SEO 友好、首屏性能优化的关键技术基石。希望本次源码剖析能帮助你更深入地理解 SSR 的工作原理。如果你对更多前端框架的底层实现感兴趣,欢迎在云栈社区继续交流探讨。