Skip to content

Commit

Permalink
fix(module:icon): debounce icon rendering on animation frame
Browse files Browse the repository at this point in the history
  • Loading branch information
arturovt committed Nov 7, 2024
1 parent d28876c commit 745eda9
Showing 1 changed file with 61 additions and 42 deletions.
103 changes: 61 additions & 42 deletions components/icon/icon.directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -11,16 +12,16 @@ import {
Input,
NgZone,
OnChanges,
OnDestroy,
OnInit,
Renderer2,
SimpleChanges,
booleanAttribute,
numberAttribute,
ExperimentalPendingTasks,
inject,
numberAttribute
DestroyRef,
PLATFORM_ID
} from '@angular/core';
import { Subject, from } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { IconDirective, ThemeType } from '@ant-design/icons-angular';

Expand All @@ -36,7 +37,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) {
Expand Down Expand Up @@ -71,7 +72,11 @@ export class NzIconDirective extends IconDirective implements OnInit, OnChanges,
private iconfont?: string;
private spin: boolean = false;

private destroy$ = new Subject<void>();
private destroyRef = inject(DestroyRef);
private pendingTasks = inject(ExperimentalPendingTasks);
private isBrowser = isPlatformBrowser(inject(PLATFORM_ID));

private destroyed = false;

constructor(
private readonly ngZone: NgZone,
Expand All @@ -82,6 +87,10 @@ export class NzIconDirective extends IconDirective implements OnInit, OnChanges,
) {
super(iconService, elementRef, renderer);

this.destroyRef.onDestroy(() => {
this.destroyed = true;
});

const iconPatch = inject(NzIconPatchService, { optional: true });
if (iconPatch) {
iconPatch.doPatch();
Expand All @@ -94,7 +103,9 @@ export class NzIconDirective extends IconDirective implements OnInit, OnChanges,
const { nzType, nzTwotoneColor, nzSpin, nzTheme, nzRotate } = changes;

if (nzType || nzTwotoneColor || nzSpin || nzTheme) {
this.changeIcon2();
// This is used to reduce the number of change detections
// while the icon is being loaded asynchronously.
this.ngZone.runOutsideAngular(() => this.changeIcon2());
} else if (nzRotate) {
this.handleRotate(this.el.firstChild as SVGElement);
} else {
Expand Down Expand Up @@ -124,46 +135,54 @@ export class NzIconDirective extends IconDirective implements OnInit, OnChanges,
}
}

ngOnDestroy(): void {
this.destroy$.next();
}

/**
* Replacement of `changeIcon` for more modifications.
*/
private changeIcon2(): void {
private async changeIcon2(): Promise<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
});
});
// 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();

try {
const svgOrRemove = await this._changeIcon();

if (this.isBrowser) {
// 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.
await new Promise(resolve => requestAnimationFrame(resolve));
}

// If the icon view has been destroyed by the time the promise resolves,
// we simply exit.
if (this.destroyed) {
return;
}

// 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);
}
});
} catch (error) {
warn(error);

Check warning on line 182 in components/icon/icon.directive.ts

View check run for this annotation

Codecov / codecov/patch

components/icon/icon.directive.ts#L182

Added line #L182 was not covered by tests
} finally {
removeTask();
}
}

private handleSpin(svg: SVGElement): void {
Expand Down

0 comments on commit 745eda9

Please sign in to comment.