Skip to content

Commit 08a3c8c

Browse files
committed
fix(module:icon): debounce icon rendering on animation frame
1 parent d28876c commit 08a3c8c

File tree

1 file changed

+50
-41
lines changed

1 file changed

+50
-41
lines changed

components/icon/icon.directive.ts

+50-41
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* found in the LICENSE file at https://github.com/NG-ZORRO/ng-zorro-antd/blob/master/LICENSE
44
*/
55

6+
import { isPlatformBrowser } from '@angular/common';
67
import {
78
AfterContentChecked,
89
ChangeDetectorRef,
@@ -11,16 +12,19 @@ import {
1112
Input,
1213
NgZone,
1314
OnChanges,
14-
OnDestroy,
1515
OnInit,
1616
Renderer2,
1717
SimpleChanges,
1818
booleanAttribute,
19+
numberAttribute,
20+
ExperimentalPendingTasks,
1921
inject,
20-
numberAttribute
22+
DestroyRef,
23+
PLATFORM_ID
2124
} from '@angular/core';
22-
import { Subject, from } from 'rxjs';
23-
import { takeUntil } from 'rxjs/operators';
25+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
26+
import { animationFrameScheduler, asapScheduler, from } from 'rxjs';
27+
import { debounceTime, finalize } from 'rxjs/operators';
2428

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

@@ -36,7 +40,7 @@ import { NzIconPatchService, NzIconService } from './icon.service';
3640
},
3741
standalone: true
3842
})
39-
export class NzIconDirective extends IconDirective implements OnInit, OnChanges, AfterContentChecked, OnDestroy {
43+
export class NzIconDirective extends IconDirective implements OnInit, OnChanges, AfterContentChecked {
4044
cacheClassName: string | null = null;
4145
@Input({ transform: booleanAttribute })
4246
set nzSpin(value: boolean) {
@@ -71,7 +75,9 @@ export class NzIconDirective extends IconDirective implements OnInit, OnChanges,
7175
private iconfont?: string;
7276
private spin: boolean = false;
7377

74-
private destroy$ = new Subject<void>();
78+
private destroyRef = inject(DestroyRef);
79+
private pendingTasks = inject(ExperimentalPendingTasks);
80+
private isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
7581

7682
constructor(
7783
private readonly ngZone: NgZone,
@@ -94,7 +100,9 @@ export class NzIconDirective extends IconDirective implements OnInit, OnChanges,
94100
const { nzType, nzTwotoneColor, nzSpin, nzTheme, nzRotate } = changes;
95101

96102
if (nzType || nzTwotoneColor || nzSpin || nzTheme) {
97-
this.changeIcon2();
103+
// This is used to reduce the number of change detections
104+
// while the icon is being loaded asynchronously.
105+
this.ngZone.runOutsideAngular(() => this.changeIcon2());
98106
} else if (nzRotate) {
99107
this.handleRotate(this.el.firstChild as SVGElement);
100108
} else {
@@ -124,46 +132,47 @@ export class NzIconDirective extends IconDirective implements OnInit, OnChanges,
124132
}
125133
}
126134

127-
ngOnDestroy(): void {
128-
this.destroy$.next();
129-
}
130-
131135
/**
132136
* Replacement of `changeIcon` for more modifications.
133137
*/
134138
private changeIcon2(): void {
135139
this.setClassName();
136140

137-
// The Angular zone is left deliberately before the SVG is set
138-
// since `_changeIcon` spawns asynchronous tasks as promise and
139-
// HTTP calls. This is used to reduce the number of change detections
140-
// while the icon is being loaded dynamically.
141-
this.ngZone.runOutsideAngular(() => {
142-
from(this._changeIcon())
143-
.pipe(takeUntil(this.destroy$))
144-
.subscribe({
145-
next: svgOrRemove => {
146-
// Get back into the Angular zone after completing all the tasks.
147-
// Since we manually run change detection locally, we have to re-enter
148-
// the zone because the change detection might also be run on other local
149-
// components, leading them to handle template functions outside of the Angular zone.
150-
this.ngZone.run(() => {
151-
// The _changeIcon method would call Renderer to remove the element of the old icon,
152-
// which would call `markElementAsRemoved` eventually,
153-
// so we should call `detectChanges` to tell Angular remove the DOM node.
154-
// #7186
155-
this.changeDetectorRef.detectChanges();
156-
157-
if (svgOrRemove) {
158-
this.setSVGData(svgOrRemove);
159-
this.handleSpin(svgOrRemove);
160-
this.handleRotate(svgOrRemove);
161-
}
162-
});
163-
},
164-
error: warn
165-
});
166-
});
141+
// It is used to hydrate the icon component property when
142+
// zoneless change detection is used in conjunction with server-side rendering.
143+
const removeTask = this.pendingTasks.add();
144+
145+
from(this._changeIcon())
146+
.pipe(
147+
// We need to individually debounce the icon rendering on each animation
148+
// frame to prevent frame drops when many icons are being rendered on the
149+
// page, such as in a `@for` loop.
150+
debounceTime(0, this.isBrowser ? animationFrameScheduler : asapScheduler),
151+
takeUntilDestroyed(this.destroyRef),
152+
finalize(removeTask)
153+
)
154+
.subscribe({
155+
next: svgOrRemove => {
156+
// Get back into the Angular zone after completing all the tasks.
157+
// Since we manually run change detection locally, we have to re-enter
158+
// the zone because the change detection might also be run on other local
159+
// components, leading them to handle template functions outside of the Angular zone.
160+
this.ngZone.run(() => {
161+
// The _changeIcon method would call Renderer to remove the element of the old icon,
162+
// which would call `markElementAsRemoved` eventually,
163+
// so we should call `detectChanges` to tell Angular remove the DOM node.
164+
// #7186
165+
this.changeDetectorRef.detectChanges();
166+
167+
if (svgOrRemove) {
168+
this.setSVGData(svgOrRemove);
169+
this.handleSpin(svgOrRemove);
170+
this.handleRotate(svgOrRemove);
171+
}
172+
});
173+
},
174+
error: warn
175+
});
167176
}
168177

169178
private handleSpin(svg: SVGElement): void {

0 commit comments

Comments
 (0)