From f918c4fcd249900e910e1af18bc1d17820ebb8e1 Mon Sep 17 00:00:00 2001 From: arturovt Date: Fri, 14 Jun 2024 20:35:30 +0300 Subject: [PATCH] fix(module:icon): debounce icon rendering on animatin frame --- components/icon/icon.directive.ts | 100 +++++++++++++++++------------- 1 file changed, 57 insertions(+), 43 deletions(-) diff --git a/components/icon/icon.directive.ts b/components/icon/icon.directive.ts index 0ff4a8adf7e..c6ab10b7f66 100644 --- a/components/icon/icon.directive.ts +++ b/components/icon/icon.directive.ts @@ -3,6 +3,7 @@ * found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE */ +import { isPlatformBrowser } from '@angular/common'; import { AfterContentChecked, ChangeDetectorRef, @@ -11,21 +12,23 @@ import { Input, NgZone, OnChanges, - OnDestroy, OnInit, Optional, Renderer2, SimpleChanges, booleanAttribute, - numberAttribute + numberAttribute, + ExperimentalPendingTasks, + inject, + DestroyRef, + PLATFORM_ID } from '@angular/core'; -import { Subject, from } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { animationFrameScheduler, asapScheduler, from } from 'rxjs'; +import { debounceTime, finalize } from 'rxjs/operators'; import { IconDirective, ThemeType } from '@ant-design/icons-angular'; -import { warn } from 'ng-zorro-antd/core/logger'; - import { NzIconPatchService, NzIconService } from './icon.service'; @Directive({ @@ -36,7 +39,7 @@ import { NzIconPatchService, NzIconService } from './icon.service'; }, standalone: true }) -export class NzIconDirective extends IconDirective implements OnInit, OnChanges, AfterContentChecked, OnDestroy { +export class NzIconDirective extends IconDirective implements OnInit, OnChanges, AfterContentChecked { cacheClassName: string | null = null; @Input({ transform: booleanAttribute }) set nzSpin(value: boolean) { @@ -71,7 +74,9 @@ export class NzIconDirective extends IconDirective implements OnInit, OnChanges, private iconfont?: string; private spin: boolean = false; - private destroy$ = new Subject(); + private destroyRef = inject(DestroyRef); + private pendingTasks = inject(ExperimentalPendingTasks); + private isBrowser = isPlatformBrowser(inject(PLATFORM_ID)); constructor( private readonly ngZone: NgZone, @@ -94,7 +99,11 @@ export class NzIconDirective extends IconDirective implements OnInit, OnChanges, const { nzType, nzTwotoneColor, nzSpin, nzTheme, nzRotate } = changes; if (nzType || nzTwotoneColor || nzSpin || nzTheme) { - this.changeIcon2(); + // The Angular zone is left deliberately before the SVG is set + // since `_changeIcon` spawns asynchronous tasks as promise and + // HTTP calls. This is used to reduce the number of change detections + // while the icon is being loaded dynamically. + this.ngZone.runOutsideAngular(() => this.changeIcon2()); } else if (nzRotate) { this.handleRotate(this.el.firstChild as SVGElement); } else { @@ -124,46 +133,51 @@ export class NzIconDirective extends IconDirective implements OnInit, OnChanges, } } - ngOnDestroy(): void { - this.destroy$.next(); - } - /** * Replacement of `changeIcon` for more modifications. */ private changeIcon2(): void { this.setClassName(); - // The Angular zone is left deliberately before the SVG is set - // since `_changeIcon` spawns asynchronous tasks as promise and - // HTTP calls. This is used to reduce the number of change detections - // while the icon is being loaded dynamically. - this.ngZone.runOutsideAngular(() => { - from(this._changeIcon()) - .pipe(takeUntil(this.destroy$)) - .subscribe({ - next: svgOrRemove => { - // Get back into the Angular zone after completing all the tasks. - // Since we manually run change detection locally, we have to re-enter - // the zone because the change detection might also be run on other local - // components, leading them to handle template functions outside of the Angular zone. - this.ngZone.run(() => { - // The _changeIcon method would call Renderer to remove the element of the old icon, - // which would call `markElementAsRemoved` eventually, - // so we should call `detectChanges` to tell Angular remove the DOM node. - // #7186 - this.changeDetectorRef.detectChanges(); - - if (svgOrRemove) { - this.setSVGData(svgOrRemove); - this.handleSpin(svgOrRemove); - this.handleRotate(svgOrRemove); - } - }); - }, - error: warn - }); - }); + // We explicitly contribute to indicating the application's + // stability by utilizing the pending tasks service. It is used + // to hydrate the icon component property when zoneless change + // detection is used in conjunction with server-side rendering. + const removeTask = this.pendingTasks.add(); + + from(this._changeIcon()) + .pipe( + // `requestAnimationFrame` is not available during server-side rendering. + // Therefore, we can use the promise-based scheduler. + // We need to individually debounce the icon rendering on each animation + // frame to prevent frame drops when many icons are being rendered on the + // page, such as in a `@for` loop. + debounceTime(0, this.isBrowser ? animationFrameScheduler : asapScheduler), + takeUntilDestroyed(this.destroyRef), + finalize(() => removeTask()) + ) + .subscribe({ + next: svgOrRemove => { + // Get back into the Angular zone after completing all the tasks. + // Since we manually run change detection locally, we have to re-enter + // the zone because the change detection might also be run on other local + // components, leading them to handle template functions outside of the Angular zone. + this.ngZone.run(() => { + // The _changeIcon method would call Renderer to remove the element of the old icon, + // which would call `markElementAsRemoved` eventually, + // so we should call `detectChanges` to tell Angular remove the DOM node. + // #7186 + this.changeDetectorRef.detectChanges(); + + if (svgOrRemove) { + this.setSVGData(svgOrRemove); + this.handleSpin(svgOrRemove); + this.handleRotate(svgOrRemove); + } + }); + }, + error: console.warn + }); } private handleSpin(svg: SVGElement): void {