Angular中Async管道为何比手动Subscribe渲染响应更慢
为什么手动Subscribe实现比Async管道版本的响应性更好?
我开发了一款基于Observable实现的员工搜索应用,逻辑十分简单,以下为两个版本的实现对比:
手动Subscribe版本
模板代码
<div class="row row-cols-1 row-cols-md-6 g-2"> <ng-container *ngIf="emps.length"> <div class="col" *ngFor="let emp of emps"> <div class="card h-100"> <img *ngIf="emp.image" [src]="emp.image" class="card-img-top" alt="image"> <div class="card-body"> <span *ngIf="emp.actived" class="badge rounded-pill bg-success" style="position: absolute;top: 2px;right: 2px;">✓ </span> <h5 class="card-title">{{emp.name| uppercase}}</h5> <!-- <pre class="card-text">{{emp.phones}}</pre> --> </div> </div> </div> </ng-container> </div>
组件代码
emps: IToList[] = []; change(){ this.service.getPeople(this.searchTerm).pipe( map(data => data.map(d => { let c = new Person(d._id, d.actived, d.name, d.phones, d.image); return c.getPerson(); })) ).subscribe(data=>this.emps=data); }
该版本运行流畅、响应及时,运行效果可参考对应演示动图。
Async管道版本
模板代码
<div class="row row-cols-1 row-cols-md-6 g-2"> <ng-container *ngIf="people$|async as emps"> <div class="col" *ngFor="let emp of emps"> <div class="card h-100"> <img *ngIf="emp.image" [src]="emp.image" class="card-img-top" alt="image"> <div class="card-body"> <span *ngIf="emp.actived" class="badge rounded-pill bg-success" style="position: absolute;top: 2px;right: 2px;">✓ </span> <h5 class="card-title">{{emp.name | uppercase}}</h5> <!-- <pre class="card-text">{{emp.phones}}</pre> --> </div> </div> </div> </ng-container> </div>
组件代码
people$!: Observable<IToList[]>; change(){ this.people$ = this.service.getPeople(this.searchTerm).pipe( map(data => data.map(d => { let c = new Person(d._id, d.attivo, d.nominativo, d.telefoni, d.immagine); return c.getPerson(); })) ); }
该版本运行时响应明显卡顿,运行效果可参考对应演示动图。
问题补充(2022年6月10日更新)
- 卡片中渲染的图片src为base64字符串,单张大小为3MB。
原因分析
卡顿和Async管道本身的性能没有任何关系,纯粹是写法差异叠加超大base64图片的渲染开销导致的,核心差异点有两个:
- 更新触发的DOM操作量差了一倍
手动subscribe版本只在接口返回数据的那一刻给emps赋值,Angular只会在数据返回后跑一次变更检测,直接渲染列表,中间没有多余的DOM操作。
你写的Async版本每次触发change()就给people$赋一个全新的Observable对象。Async管道只要检测到传入的Observable引用变化,会立刻执行三个动作:取消旧Observable的订阅、把当前持有的渲染值清空为null、订阅新的Observable。这时候Angular检测到people$|async的结果变成null,会先把整个已经渲染好的员工列表DOM全部删除;等新Observable的接口数据返回,Async管道拿到新数据,Angular又要从头创建所有卡片DOM,等于每次搜索都要多跑一轮「删除全部旧DOM→重建全部新DOM」的操作,平白多了一倍的DOM开销。 - 3MB的base64大图把开销放大到了可感知卡顿的程度
单张3MB的base64图片,从DOM挂载、格式解码到绘制到页面上,每一步都要占用主线程资源,还要消耗大量内存。手动subscribe版本里,你直接替换emps数组的引用,*ngFor默认按对象引用跟踪列表项,如果两次搜索结果里包含同一个员工,对应的DOM节点会被直接复用,连图片都不需要重新解码绘制。但你写的Async版本每次都先把旧DOM全部删除,浏览器要先回收这些大图片占用的内存,等新DOM创建的时候又要重新解码、重新绘制所有图片,主线程直接被占满,自然会出现明显卡顿。
优化方案
- 不要每次搜索都给
people$重新赋值,改用Subject推送搜索关键词,在组件初始化阶段就定义好Observable流,只让Async管道订阅一次:
private searchTerm$ = new Subject<string>(); people$!: Observable<IToList[]>; ngOnInit() { this.people$ = this.searchTerm$.pipe( debounceTime(300), // 加防抖避免输入时频繁发请求 distinctUntilChanged(), // 搜索词没变化就不重复发请求 switchMap(term => this.service.getPeople(term).pipe( map(data => data.map(d => { const c = new Person(d._id, d.actived, d.name, d.phones, d.image); return c.getPerson(); })) )) ); } change() { this.searchTerm$.next(this.searchTerm); }
- 给
*ngFor添加自定义trackBy函数,按员工唯一ID跟踪列表项,就算数据更新也复用已有DOM节点,避免重复销毁重建:
<div class="col" *ngFor="let emp of emps; trackBy: trackByEmpId">
trackByEmpId(index: number, emp: IToList) { return emp._id; }
- 不要直接用3MB的base64字符串作为图片源,尽量压缩图片体积、转成WebP等高效格式,配合图片懒加载减少主线程渲染压力。
内容的提问来源于stack exchange,提问作者Kraken
相关产品推荐
相关产品推荐

