在聊Angular的SSR之前,有必要把视角稍微拉远一点:浏览器到底在做什么,以及SSR在浏览器渲染链路里究竟改变了哪几件关键事情。
浏览器拿到一个HTML文档后,会经历一条相当固定的路径:解析HTML构建DOM树,解析CSS构建CSSOM,把两者合成渲染树,计算布局,绘制,再合成到屏幕。CSR(纯客户端渲染)的问题不在于浏览器不会渲染,而在于浏览器最开始拿到的HTML往往只有一个空壳容器,真正的内容要等JavaScript下载、解析、执行完毕,再由框架把DOM生成出来。结果就是用户看到首屏内容的时间被推迟,搜索引擎抓取与社交分享的meta信息也更容易出现缺失或不稳定。
Angular的SSR解决的核心矛盾很直接:把Angular的首屏DOM生成这件事,从浏览器挪到服务器端去做,让浏览器一开始就拿到带内容的HTML,随后再把交互能力补回来,这个补回交互的过程就是Hydration(水合)。官方对Hydration的描述非常清晰:它要复用服务器端已经渲染出来的DOM结构、恢复应用状态、把服务器端获取的数据转交给客户端,避免重复请求等。(Angular)
下面按你关心的点来拆解:Angular用了哪些技术实现SSR,这些技术在架构里分别扮演什么角色,以及在真实项目里如何组合它们。
Angular SSR在今天的定位:Hybrid Rendering而不是单一SSR
从Angular的官方文档体系看,SSR现在被放进了更大的概念Hybrid Rendering(混合渲染)里:同一个应用里,你可以对不同路由选择SSR、SSG(预渲染)或CSR,以便在SEO、首屏性能、个性化和服务器成本之间做更细颗粒度的权衡。官方给出的三种RenderMode也很明确:Server(按请求服务器渲染)、Client(浏览器端渲染)、Prerender(构建期生成静态HTML)。(Angular)
更重要的是,现在启用这套能力的路径已经非常产品化:新项目可以用ng new --ssr,已有项目用ng add @angular/ssr,这在官方Hybrid Rendering指南里就是推荐做法。(Angular)
技术栈总览:Angular SSR由哪几层组成
把Angular SSR拆成工程可落地的组件,通常会落到七层:
- 构建与产物层:
Angular的新构建系统负责把同一套源码编译成浏览器产物与服务器产物,并把SSR、Prerender的工作流整合进CLI。 - 服务器运行时层:最常见是
Node.js,也可以是非Node.js的运行时(只要能提供类Fetch的Request/Response语义)。 - 服务器端渲染引擎层:把
Angular应用启动起来并渲染为字符串HTML,典型代表是renderApplication与CommonEngine。 - 路由与渲染模式编排层:对不同路由选择
SSR/SSG/CSR,并控制构建期或请求期的渲染策略。 - 客户端恢复层:
Hydration,负责把服务器端HTML变成可交互应用,包含事件回放、增量水合等能力。 - 数据传递与缓存层:把服务器端请求结果带到客户端,避免
Hydration后二次HTTP请求,核心是HttpClient传输缓存与TransferState思路。 - 跨平台兼容层:解决
window/document等浏览器专属对象缺失的问题,包含平台判断、延后执行钩子、必要时的DOM模拟(如domino)。
接下来逐层展开。
构建与产物层:新构建系统esbuild+Vite,把SSR变成一等公民
从Angular 17起,Angular的新构建系统逐步稳定并成为主流路径。官方对这套系统的描述里,有几句非常关键:它使用ESM输出格式、引入esbuild与Vite等现代工具,并且集成了SSR与Prerendering能力。(Angular)
这意味着两件事:
SSR不再是额外拼装的脚手架,而是编译链路原生支持的目标之一。- 应用在构建后会形成面向浏览器与面向服务器的两份运行产物,
CLI可以直接用这些产物执行SSR或SSG。
在团队协作里,这个变化的价值非常现实:以前SSR常常意味着你要维护一套特殊的webpack配置、单独的服务器编译流程、以及跟主工程不同步的构建参数;现在大多数项目能把这些复杂度交还给Angular CLI。
服务器端渲染引擎层:renderApplication与CommonEngine是两块核心积木
renderApplication:最底层的SSR能力
从 API 语义上看,renderApplication做的事情非常直白:引导启动一个Angular应用实例,并把它渲染成字符串HTML。(Angular)
你可以把它理解成Angular在服务器端的bootstrapApplication + serialize DOM的组合。它解决的是SSR的最核心问题:在没有真实浏览器的环境里,如何把组件树跑一遍、生成首屏DOM,并把结果序列化成HTML。
CommonEngine:面向Node.js应用的通用渲染引擎
在真实项目里,你更常见到的是CommonEngine。官方 API 把它定义为A common engine to use to server render an application,并提供render方法返回渲染好的HTML。(Angular)
CommonEngine的优势在于它把很多工程细节封装好了:文档模板路径、目标url、静态资源publicPath、平台级providers注入等,适合跟Express或其它Node框架整合。
服务器运行时层:Node.js默认方案,以及非Node.js的@angular/ssr
Node.js+Express:最常见的落地组合
官方SSR指南里给了一个非常典型的server.ts架构:用Express托管静态资源,对普通路由调用CommonEngine.render,把生成的HTML返回给浏览器。文档还点出一个容易被忽视的细节:从Angular 17开始,ng serve不再依赖server.ts,开发服务器会直接使用main.server.ts执行服务器端渲染。(v19.angular.dev)
这件事对开发体验的影响很大:你不需要每次都把Express服务器跑起来才看得到SSR的效果,CLI会把SSR融进常规的开发工作流。
一个更贴近现代Angular风格(ESM、更少脚手架依赖)的server.ts通常会长这样(示例代码用单引号,避免引入英文双引号):
import{APP_BASE_HREF}from'@angular/common';import{CommonEngine}from'@angular/ssr/node';importexpressfrom'express';import{dirname,join,resolve}from'node:path';import{fileURLToPath}from'node:url';importbootstrapfrom'./src/main.server';exportfunctionapp():express.Express{constserver=express();constserverDistFolder=dirname(fileURLToPath(import.meta.url));constbrowserDistFolder=resolve(serverDistFolder,'../browser');constindexHtml=join(serverDistFolder,'index.server.html');constengine=newCommonEngine();server.set('view engine','html');server.set('views',browserDistFolder);server.get('*.*',express.static(browserDistFolder,{maxAge:'1y'}));server.get('*',(req,res,next)=>{const{protocol,originalUrl,headers}=req;engine.render({bootstrap,documentFilePath:indexHtml,url:`${protocol}://${headers.host}${originalUrl}`,publicPath:browserDistFolder,providers:[{provide:APP_BASE_HREF,useValue:req.baseUrl}],}).then(html=>res.send(html)).catch(next);});returnserver;}你会发现它本质上是三段式:
- 静态资源交给
Express; - 动态路由走
CommonEngine.render; - 通过
providers把请求上下文(如APP_BASE_HREF)注入到Angular的依赖注入系统里。
这段整体结构与官方示例一致。(v19.angular.dev)
非Node.js:@angular/ssr用Web API的Request/Response语义做适配
当你把SSR部署到一些更偏Edge的运行环境,比如支持Fetch标准的Serverless平台时,Node.js的req/res并不是天然存在的。官方Hybrid Rendering指南明确提到:@angular/ssr提供了在非Node.js平台做服务器端渲染的关键 API,并基于标准Web API的Request与Response对象来集成。(Angular)
这个设计背后的思路很像浏览器内核的演进路线:尽量围绕标准化的Web Platform API做抽象层,减少对某一种服务器实现的绑定。对架构师来说,这等于给部署形态留下了更大的弹性空间。
顺带一提,从npm的版本信息可以看到@angular/ssr在 2025 年底仍在快速迭代(截至 2026 年 1 月,最新版已到21.x)。(npm)
路由与渲染模式编排层:把SSR、SSG、CSR做成可配置策略
Hybrid Rendering最有工程价值的一点,是把渲染策略提升为路由级别的配置,而不是全站一刀切。官方文档用RenderMode来描述不同路由的渲染模式:Server、Client、Prerender。(Angular)
把它翻译成真实世界的产品逻辑,大概是这样:
- 营销落地页、活动页:内容相对稳定,追求极致首屏与缓存命中,适合
Prerender(SSG)。 - 商品详情、文章详情:内容变化频繁但又强依赖
SEO,适合Server(请求期SSR)。 - 登录后控制台、内部管理界面:
SEO不重要,更在意交互与开发效率,Client(CSR)很合理。
官方还提到一个配置点:默认情况下Angular会对整个应用执行Prerender并生成 server 文件;若你要生成完全静态站点,可以设置outputMode为static。(Angular)
这类能力一旦落到团队开发,会显著减少争论:不是讨论SSR要不要上,而是讨论某一组路由到底用SSR还是SSG,边界清晰很多。
客户端恢复层:Hydration、事件回放与增量水合
如果把SSR比作把菜提前端上桌,那么Hydration就是把桌上的菜变成可以吃的状态:绑定事件、恢复状态、让组件树重新接管DOM。
provideClientHydration:水合的入口与默认能力集合
Angular的provideClientHydration是启用水合的核心 API。官方说明它默认启用一组推荐特性,包含DOM的协调式水合(reconciling)以及服务器端运行时的HttpClient响应缓存并传递给客户端,避免重复请求。(Angular)
这两点非常关键:
- 协调式水合意味着客户端不会粗暴地丢掉服务器端
DOM重建,而是尽量复用已有结构,减少首屏闪烁与重排。 HttpClient传输缓存意味着你在服务器端SSR时请求过的数据,客户端水合阶段可以直接复用,减少瀑布流请求带来的LCP波动。
事件回放withEventReplay:解决水合窗口期的交互丢失
现实体验里,一个常见问题是:页面已经有HTML了,用户看到按钮就会点,但此时水合尚未完成,事件监听器还没挂上。Angular的withEventReplay就是为这个窗口期设计的:在水合完成前捕获用户事件(比如click),等水合完成后再回放执行。(Angular)
这在电商类站点特别实用:用户往往在首屏刚出来就点筛选、点加入购物车,事件回放能显著减少SSR页面给人的假可点体验。
增量水合withIncrementalHydration:把水合从一次性变成按需
当应用很大、组件树很深时,全量水合会带来明显的主线程压力。Angular提供了Incremental Hydration,让水合按一定策略分批进行。官方文档还提到:增量水合依赖并会自动启用事件回放,如果你已经启用withEventReplay,开启增量水合后可以移除前者。(Angular)
把这点放到浏览器内核视角,它的意义在于:减少一次性JS执行与事件绑定造成的长任务,让交互恢复更平滑,避免把首屏可交互时间推迟太多。
数据传递与缓存层:Http Transfer Cache与可控的缓存策略
SSR的另一个高频痛点是:服务器端渲染时请求了一遍数据,客户端启动后又请求一遍,既浪费带宽也影响性能。Angular官方给出的路径更偏框架级方案:通过水合体系自带的HttpClient缓存,把服务器端响应带到客户端。(Angular)
你可以用withHttpTransferCacheOptions来控制缓存策略,比如包含哪些请求头、是否缓存POST、通过filter决定哪些请求进入缓存。(Angular)
一个实战化的配置示例(仍然避免英文双引号):
import{provideClientHydration,withHttpTransferCacheOptions}from'@angular/platform-browser';exportconstappConfig={providers:[provideClientHydration(withHttpTransferCacheOptions({includeHeaders:['X-Trace-Id'],includePostRequests:false,filter:req=>req.method==='GET'&&req.url.includes('/api/'),}),),],};真实项目里,这样的过滤策略通常会配合接口分层:
/api/public/*:适合缓存并传递;/api/user/*:含鉴权或个性化信息,谨慎传递,甚至直接禁用;/api/checkout/*:强一致性与安全优先,一般不做传递缓存。
跨平台兼容层:浏览器不是服务器,服务器也不该假装成浏览器
Angular官方在SSR文档里专门强调了一个事实:在服务器上不能使用window、document、navigator、location这类浏览器全局对象,也不能依赖某些HTMLElement属性。(Angular)
这条规则看似简单,但它对应的坑非常真实:你在组件构造函数里读了window.innerWidth,本地CSR完全正常,一上SSR就直接ReferenceError。
平台判断:isPlatformBrowser是最基础的开关
官方提供的isPlatformBrowser用来判断当前平台是否为浏览器。(Angular)
一种工程上更可维护的写法,是把平台判断封装到服务里,组件只依赖服务,而不是到处散落if。这样做的好处是将来如果你切到Zoneless或者引入Edge SSR,改动面更小。
import{Injectable,inject}from'@angular/core';import{PLATFORM_ID}from'@angular/core';import{isPlatformBrowser}from'@angular/common';@Injectable({providedIn:'root'})exportclassPlatformService{privatereadonlyplatformId=inject(PLATFORM_ID);getisBrowser():boolean{returnisPlatformBrowser(this.platformId);}}延后到浏览器阶段执行:afterNextRender等钩子更贴合SSR语义
很多逻辑并不是非要写平台判断,而是它本质上就应该发生在浏览器首帧渲染之后,比如初始化图表、读取真实布局尺寸、绑定第三方DOM插件。Angular提供了afterNextRender这类 API,并明确指出它只会在浏览器平台运行。(Angular)
这类 API 的思路很像浏览器渲染管线里的post paint任务:把必须依赖真实DOM的操作推迟到正确的时间点执行,而不是在SSR阶段硬凑一个window出来。
DOM模拟层:当你必须在服务器上满足某些DOM依赖时,domino是典型工具
有些第三方库写得很强势:加载时就直接访问document或window,根本不给你在业务代码里做平台判断的机会。这个时候,工程上常见的补丁方案就是DOM模拟库,比如domino。
社区教程对domino的定位很明确:在Angular SSR场景下,如果你需要访问DOMAPI,可以使用domino来提供querySelector等能力。(danywalls.com)
但这里要强调一个架构层面的判断:domino更像是兼容性垫片,而不是理想解。原因很简单:模拟DOM永远不可能完整覆盖真实浏览器的布局与渲染行为,它只能让代码不崩,不能保证视觉与交互逻辑一致。实践里更推荐的路线通常是:
- 能替换库就替换成
SSR friendly的版本; - 不能替换就做平台隔离与懒加载;
- 真的没办法,再上
domino,并把它控制在尽量小的范围内。
Angular Universal在今天的角色:从核心方案降级为兼容路径
很多团队仍然在用Angular Universal的Express引擎。@nguniversal/express-engine在npm上的描述很直接:这是一个Express Engine,用于在服务器上运行Angular应用以实现SSR。(npm)
把它放到今天的语境里,可以把Angular SSR的演进理解为:
- 早期:
Angular Universal是主力方案,ng add @nguniversal/express-engine负责把SSR脚手架搭起来。 - 现在:
SSR能力更多被@angular/ssr与新构建系统吸收,Universal在不少项目里承担的是迁移期兼容与历史包袱承接。
这对架构决策的影响是:新项目更倾向直接走@angular/ssr与官方的Hybrid Rendering流程;存量项目则根据成本选择渐进迁移。
真实世界案例:一个电商站点如何用Angular SSR把性能与成本压到合理区间
假设你在做一个跨境电商站点,业务目标有三条:
- 商品详情页要有稳定的
SEO与社交分享卡片; - 首屏要快,
LCP要稳; - 服务器成本不能炸,不能所有页面都请求期
SSR。
一套常见而有效的组合是:
/、/campaign/*、/about:用Prerender(SSG)。这些页面变化不频繁,适合直接上CDN,命中率极高。(Angular)/product/:id、/category/:id:用Server(请求期SSR)。需要对不同商品动态生成内容,同时对搜索引擎友好。(Angular)/account/*、/admin/*:用Client(CSR)。对SEO没要求,优先交互体验与开发效率。(Angular)
客户端水合层面,开启:
provideClientHydration:基础水合与HttpClient传输缓存。(Angular)withEventReplay或直接上withIncrementalHydration:避免用户在首屏点了按钮却没反应。(Angular)withHttpTransferCacheOptions:只缓存商品与类目接口,过滤掉用户态接口。(Angular)
跨平台兼容层面:
- 所有依赖
window的逻辑都放在浏览器阶段执行,要么用isPlatformBrowser保护,要么用afterNextRender延迟。(Angular)
你会发现,这套方案的本质是一种工程化的分层渲染:把最贵的请求期SSR留给真正需要的页面,把能静态化的页面尽量静态化,把交互恢复做得足够平滑,避免SSR变成新的体验问题。
什么时候SSR反而不该用:一条务实的判断线
再强调一次:SSR不等于无脑更好,它会带来服务器运行成本、构建复杂度、缓存策略复杂度、以及第三方库兼容成本。哪怕是很早期的Angular SSR文章也反复提醒过:只有在确实需要SSR优势时才值得引入这份复杂度。(ANGULARarchitects)
一个比较务实的判断线是:
- 强
SEO、强分享卡片、首屏必须稳:SSR或SSG几乎必选。 - 页面高度个性化且强实时:倾向请求期
SSR,但要做缓存分层与降级策略。 - 登录后系统、内部工具:多数情况下
CSR足够,SSR的收益很难覆盖成本。
小结:回答Angular用了哪些技术实现SSR
把全文收束成清单,你问的Angular用了哪些技术实现SSR,可以概括为这些关键点:
- 框架级服务器渲染能力:
@angular/platform-server提供renderApplication把应用渲染成字符串HTML。(Angular) - 面向服务器的渲染引擎封装:
CommonEngine作为通用渲染引擎,负责把url、模板、静态资源路径、依赖注入等工程要素串起来。(Angular) - 运行时与适配层:默认
Node.js+Express,并通过@angular/ssr支持基于Web API Request/Response的非Node.js平台集成。(v19.angular.dev) - 构建系统支撑:新构建系统引入
esbuild与Vite,并原生集成SSR与Prerender工作流。(Angular) - 混合渲染编排:用
RenderMode在路由级别选择SSR/SSG/CSR,并通过outputMode等配置控制生成形态。(Angular) - 客户端恢复与体验增强:
provideClientHydration提供水合能力,配合HttpClient传输缓存;withEventReplay解决水合窗口期事件丢失;withIncrementalHydration把水合拆成增量过程。(Angular) - 数据传递与缓存控制:
withHttpTransferCacheOptions与HttpTransferCacheOptions提供可配置缓存策略,减少重复请求。(Angular) - 跨平台兼容与必要的
DOM模拟:官方明确禁止在服务器使用window等对象,需要用平台判断与延迟执行;在极端情况下可用domino做DOMAPI 垫片。(Angular) - 历史兼容路径:
Angular Universal的@nguniversal/express-engine仍在大量存量项目中使用,更多承担迁移与兼容角色。(npm)
如果你愿意,我也可以按一个你熟悉的项目类型(比如企业后台、内容站、跨境电商、B2B门户)给出一份更落地的SSR架构蓝图:路由渲染模式划分、缓存分层、数据预取策略、以及如何在不牺牲开发体验的前提下把SSR纳入日常迭代。