Angular Universal SSR 首次页面加载两次问题如何解决?
Angular Universal SSR页面重复加载问题解决方案
根因说明
你遇到的两种表现本质是Angular SSR水合(Hydration)的默认行为导致:
- 无
*ngIf="isBrowser"限制时,服务端返回完整渲染的HTML,客户端Angular实例启动后,默认会销毁服务端生成的DOM,重新渲染整棵组件树,因此出现两次加载的视觉效果 - 加了
*ngIf="isBrowser"限制后,服务端不会渲染该部分内容,SSR能力失效,自然页面源码看不到对应结构
具体解决步骤
1. 启用官方水合特性(Angular 16+ 优先使用)
Angular 16及以上版本内置了稳定的水合能力,开启后客户端会直接复用服务端生成的DOM,不会整树销毁重绘,彻底解决重复加载问题,配置方式如下:
// 独立API模式下在app.config.ts中配置 import { provideClientHydration } from '@angular/platform-browser'; import { ApplicationConfig } from '@angular/core'; export const appConfig: ApplicationConfig = { providers: [ // 其他已有provider provideClientHydration() ] }; // NgModule模式下在AppModule中配置 import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; @NgModule({ imports: [ // 确保服务端和客户端的appId完全一致 BrowserModule.withServerTransition({ appId: 'your-app-unique-id' }), // 其他已有import ] }) export class AppModule {}
2. 移除不必要的isBrowser判断
删掉router-outlet外层的*ngIf="this.isBrowser"判断,保证服务端可以正常渲染该部分内容,页面源码就能看到完整结构。
3. 修复服务端/客户端逻辑不一致的问题
很多时候重复加载是因为服务端和客户端渲染出来的DOM结构不一致,Angular检测到差异后强制重绘导致,需要做以下检查:
- 所有调用浏览器专属API(window、document、localStorage等)的代码,都要包裹在平台判断逻辑中:
import { PLATFORM_ID, Inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; constructor(@Inject(PLATFORM_ID) private platformId: object) { if (isPlatformBrowser(this.platformId)) { // 仅在客户端执行的逻辑,比如操作window、绑定滚动事件等 } } - 检查
onActivate方法中有没有强制触发全局变更检测、路由重定向这类会导致整树重绘的逻辑,调整为按需执行。
4. 低版本Angular(16以下)额外配置数据缓存
低版本没有内置水合能力时,大概率的重复加载是因为服务端请求了一次数据渲染页面,客户端启动后又重复请求了一次数据,页面内容刷新导致视觉上的两次加载,用TransferState缓存服务端请求的数据即可:
// 数据请求服务示例 import { TransferState, makeStateKey } from '@angular/platform-browser'; import { isPlatformBrowser, isPlatformServer } from '@angular/common'; import { PLATFORM_ID, Inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { of } from 'rxjs'; import { tap } from 'rxjs/operators'; const HOME_DATA_KEY = makeStateKey('home-page-data'); @Injectable({ providedIn: 'root' }) export class HomeDataService { constructor( private http: HttpClient, private transferState: TransferState, @Inject(PLATFORM_ID) private platformId: object ) {} getHomeData() { // 客户端优先读取服务端缓存的 data,不重复发请求 if (isPlatformBrowser(this.platformId) && this.transferState.hasKey(HOME_DATA_KEY)) { const data = this.transferState.get(HOME_DATA_KEY, null); this.transferState.remove(HOME_DATA_KEY); return of(data); } // 发起请求获取数据 return this.http.get('/api/home-page-data').pipe( tap(data => { // 服务端将请求到的数据存入缓存,随HTML返回给客户端 if (isPlatformServer(this.platformId)) { this.transferState.set(HOME_DATA_KEY, data); } }) ); } }
同时需要分别在AppModule中导入BrowserTransferStateModule,服务端模块中导入ServerTransferStateModule。
内容的提问来源于stack exchange,提问作者Vichakshan Technologies
相关产品推荐
相关产品推荐

