SSR与CSR,是前端开发者再熟悉不过的两种渲染模式。它们的核心区别在于:SSR由服务端直接返回包含首屏内容的HTML;而CSR则由服务端返回一个空根节点,再由浏览器解析并执行JavaScript来填充内容。因此,两者的主要渲染时间差就产生在这里,SSR无需等待JS解析便可直接呈现内容。
那么,业界优秀的支持SSR的框架究竟是如何实现的呢?本文将以Alibaba的开源框架ice.js为例,进行源码级的剖析,探究SSR的实现机制、以及与CSR的共性和差异。
SSR的实现
实现SSR必备的是一台能够解析SSR脚本的服务器。相比之下,CSR只需进行静态资源托管即可,每次构建生成的HTML文件是固定的,直接由站点托管访问(SSG模式也是如此)。
那么,解析SSR脚本这件事,在ice.js框架里主要做了什么呢?我们可以先进行一个大致的构思:
- 可能有一个
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)对象。
- 处理前端组件到HTML的转换。
- 直接进行HTTP响应或流式响应。
- 出现异常时,降级为CSR渲染,返回空节点。
至此,我们的多个猜想都得到了验证:网络请求、响应(HTTP/Stream)都已找到。接下来看看 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,
},
};
}
这个函数是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:构建包含React严格模式、全局异常捕获和路由能力的应用骨架。
const documentContext = {
main: <App
action={Action.Pop}
location={location}
navigator={staticNavigator}
static
RouteWrappers={RouteWrappers}
AppRouter={AppRouter}
/>,
};
- 组装完整的HTML组件树
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,一次完整的服务端渲染就此完成。这个运行在Node.js环境下的服务端渲染流程清晰展示了如何将React组件树转换为HTML流并发送给客户端。
/**
* 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 的中间件机制,拦截请求并直接触发渲染。我们来细看 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,
};
}
其核心逻辑与服务端主渲染函数并无本质区别。巧妙之处在于,它基于 appConfig 配置(判断是否为SSR模式),在 webpack dev server 的中间件链上加了一层拦截。如果是SSR模式,则动态引入服务端渲染函数,直接走完整的SSR渲染链路(即上一节分析的链路);否则,请求就继续走常规的前端框架静态资源构建和CSR链路。这种设计实现了开发环境下SSR与CSR的无缝切换和高效热更新。
总结
通过以上对ice.js框架SSR实现机制的源码级剖析,我们可以清晰地看到,一个现代SSR框架的核心在于:在服务端构建完整的React应用上下文与组件树,并利用 react-dom/server 的能力将其渲染为HTML。同时,框架需要精巧地处理路由匹配、数据获取、流式响应以及开发时的构建集成。理解这些原理,有助于我们在实践中更好地驾驭SSR技术,优化应用性能与体验。