Skip to content

Commit

Permalink
perf(module:select): do not run change detection if the `triggerWidth…
Browse files Browse the repository at this point in the history
…` has not been changed
  • Loading branch information
arturovt committed Nov 2, 2021
1 parent b18c050 commit a3deb0e
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 4 deletions.
17 changes: 14 additions & 3 deletions components/select/select.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { startWith, switchMap, takeUntil } from 'rxjs/operators';
import { slideMotion } from 'ng-zorro-antd/core/animation';
import { NzConfigKey, NzConfigService, WithConfig } from 'ng-zorro-antd/core/config';
import { NzNoAnimationDirective } from 'ng-zorro-antd/core/no-animation';
import { reqAnimFrame } from 'ng-zorro-antd/core/polyfill';
import { cancelRequestAnimationFrame, reqAnimFrame } from 'ng-zorro-antd/core/polyfill';
import { NzDestroyService } from 'ng-zorro-antd/core/services';
import { BooleanInput, NzSafeAny, OnChangeType, OnTouchedType } from 'ng-zorro-antd/core/types';
import { InputBoolean, isNotNil } from 'ng-zorro-antd/core/util';
Expand Down Expand Up @@ -242,6 +242,7 @@ export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterCon
private isReactiveDriven = false;
private value: NzSafeAny | NzSafeAny[];
private _nzShowArrow: boolean | undefined;
private requestId: number = -1;
onChange: OnChangeType = () => {};
onTouched: OnTouchedType = () => {};
dropDownPosition: 'top' | 'center' | 'bottom' = 'bottom';
Expand Down Expand Up @@ -491,9 +492,18 @@ export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterCon

updateCdkConnectedOverlayStatus(): void {
if (this.platform.isBrowser && this.originElement.nativeElement) {
reqAnimFrame(() => {
const triggerWidth = this.triggerWidth;
cancelRequestAnimationFrame(this.requestId);
this.requestId = reqAnimFrame(() => {
// Blink triggers style and layout pipelines anytime the `getBoundingClientRect()` is called, which may cause a
// frame drop. That's why it's scheduled through the `requestAnimationFrame` to unload the composite thread.
this.triggerWidth = this.originElement.nativeElement.getBoundingClientRect().width;
this.cdr.markForCheck();
if (triggerWidth !== this.triggerWidth) {
// The `requestAnimationFrame` will trigger change detection, but we're inside an `OnPush` component which won't have
// the `ChecksEnabled` state. Calling `markForCheck()` will allow Angular to run the change detection from the root component
// down to the `nz-select`. But we'll trigger only local change detection if the `triggerWidth` has been changed.
this.cdr.detectChanges();
}
});
}
}
Expand Down Expand Up @@ -672,6 +682,7 @@ export class NzSelectComponent implements ControlValueAccessor, OnInit, AfterCon
}
}
ngOnDestroy(): void {
cancelRequestAnimationFrame(this.requestId);
this.focusMonitor.stopMonitoring(this.elementRef);
}
}
46 changes: 45 additions & 1 deletion components/select/select.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { BACKSPACE, DOWN_ARROW, ENTER, ESCAPE, SPACE, TAB, UP_ARROW } from '@angular/cdk/keycodes';
import { OverlayContainer } from '@angular/cdk/overlay';
import { Component, TemplateRef, ViewChild } from '@angular/core';
import { ComponentFixture, fakeAsync, flush, inject } from '@angular/core/testing';
import { ComponentFixture, fakeAsync, flush, inject, tick } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';

Expand Down Expand Up @@ -1180,6 +1180,50 @@ describe('select', () => {
expect(listOfItem[2].textContent).toBe('and 2 more selected');
}));
});
describe('change detection', () => {
let testBed: ComponentBed<TestSelectTemplateDefaultComponent>;
let component: TestSelectTemplateDefaultComponent;
let fixture: ComponentFixture<TestSelectTemplateDefaultComponent>;
let selectComponent: NzSelectComponent;
let overlayContainerElement: HTMLElement;

beforeEach(() => {
testBed = createComponentBed(TestSelectTemplateDefaultComponent, {
imports: [NzSelectModule, NzIconTestModule, FormsModule]
});
component = testBed.component;
fixture = testBed.fixture;
selectComponent = testBed.debugElement.query(By.directive(NzSelectComponent)).componentInstance;
});

beforeEach(inject([OverlayContainer], (oc: OverlayContainer) => {
overlayContainerElement = oc.getContainerElement();
}));

it('should not run change detection if the `triggerWidth` has not been changed', fakeAsync(() => {
const detectChangesSpy = spyOn(selectComponent['cdr'], 'detectChanges').and.callThrough();
const requestAnimationFrameSpy = spyOn(window, 'requestAnimationFrame').and.callThrough();

component.nzOpen = true;
fixture.detectChanges();
// The `requestAnimationFrame` is simulated as `setTimeout(..., 16)` inside the `fakeAsync`.
tick(16);

dispatchKeyboardEvent(overlayContainerElement, 'keydown', ESCAPE, overlayContainerElement);
fixture.detectChanges();
flush();

expect(component.nzOpen).toEqual(false);

component.nzOpen = true;
fixture.detectChanges();
tick(16);

// Ensure that the `detectChanges()` have been called only once since the `triggerWidth` hasn't been changed.
expect(detectChangesSpy).toHaveBeenCalledTimes(1);
expect(requestAnimationFrameSpy).toHaveBeenCalledTimes(2);
}));
});
});

@Component({
Expand Down

0 comments on commit a3deb0e

Please sign in to comment.