Skip to content

Commit bae85ea

Browse files
MayaKirovaMKirovarkaraivanov
authored
fix(perf): Apply base grid scroll performance optimizations. (#16708)
--------- Co-authored-by: MKirova <MKirova@DEV-MKIROVA> Co-authored-by: Radoslav Karaivanov <rkaraivanov@infragistics.com>
1 parent 9848e51 commit bae85ea

28 files changed

Lines changed: 275 additions & 115 deletions

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
All notable changes for each version of this project will be documented in this file.
44

5+
## 21.2.0
6+
7+
### Breaking Changes
8+
9+
- `igxForOf`, `igxGrid`, `igxTreeGrid`, `igxHierarchicalGrid`, `igxPivotGrid`
10+
- original `data` array mutations (like adding/removing/moving records in the original array) are no longer detected automatically. Components need an array ref change for the change to be detected.
11+
512
## 21.1.0
613

714
### New Features

projects/igniteui-angular-performance/src/app/app.component.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
{{ route.title }}
66
</button>
77
}
8+
9+
<button igxButton="contained" (click)="OnPerfTest()">
10+
Test scroll performance
11+
</button>
812
</div>
913
<router-outlet />
1014
</main>

projects/igniteui-angular-performance/src/app/app.component.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component } from '@angular/core';
1+
import { Component, ViewChild } from '@angular/core';
22
import { RouterLink, RouterOutlet, Routes } from '@angular/router';
33
import { IgxButtonDirective } from 'igniteui-angular';
44
import { routes } from './app.routes';
@@ -11,4 +11,24 @@ import { routes } from './app.routes';
1111
})
1212
export class AppComponent {
1313
protected routes: Routes = routes;
14+
15+
@ViewChild(RouterOutlet) outlet!: RouterOutlet;
16+
17+
public async OnPerfTest() {
18+
const longTask = [];
19+
const observer = new PerformanceObserver((list) => {
20+
longTask.push(...list.getEntries());
21+
});
22+
observer.observe({ entryTypes: ['longtask'] });
23+
const grid = (this.outlet.component as any).grid || (this.outlet.component as any).pivotGrid;
24+
for (let i = 0; i < 100; i++) {
25+
grid.navigateTo(i * 50);
26+
await new Promise(r => setTimeout(r, 50));
27+
}
28+
const sum = longTask.reduce((acc, task) => acc + task.duration, 0);
29+
const avgTime = sum / longTask.length;
30+
console.log('Long Tasks:'+ longTask.length + ", ", 'Average Long Task Time:', avgTime);
31+
observer.disconnect();
32+
33+
}
1434
}

projects/igniteui-angular-performance/src/app/grid/grid.component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<div class="grid-wrapper">
22
<igx-grid
3+
34
#grid
45
[data]="data"
5-
[allowFiltering]="true"
6+
[allowFiltering]="false"
67
[height]="'100%'"
78
[width]="'100%'"
89
>

projects/igniteui-angular-performance/src/app/pivot-grid/pivot-grid.component.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,9 @@ export class PivotGridComponent {
182182
sortDirection: SortingDirection.None
183183
},
184184
{
185-
fullDate: false,
185+
fullDate: true,
186186
quarters: true,
187-
months: false,
187+
months: true,
188188
}),
189189
],
190190
values: [

projects/igniteui-angular-performance/src/styles.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@use '../../igniteui-angular/src/lib/core/styles/themes' as *;
1+
@use '../../igniteui-angular/core/src/core/styles/themes' as *;
22
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
33
@include core();
44
@include typography(

projects/igniteui-angular/directives/src/directives/for-of/base.helper.component.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,6 @@ export class VirtualHelperBaseDirective implements OnDestroy, AfterViewInit {
4343
this._scrollNativeSize = this.calculateScrollNativeSize();
4444
}
4545

46-
@HostListener('scroll', ['$event'])
47-
public onScroll(event) {
48-
this.scrollAmount = event.target.scrollTop || event.target.scrollLeft;
49-
}
50-
5146

5247
public ngAfterViewInit() {
5348
this._afterViewInit = true;

projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.spec.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,15 +249,17 @@ describe('IgxForOf directive -', () => {
249249
it('should update vertical scroll offsets if igxForOf changes. ', () => {
250250
fix.componentInstance.scrollTop(5);
251251
fix.detectChanges();
252+
let transform = displayContainer.style.transform;
252253

253-
expect(parseInt(displayContainer.style.top, 10)).toEqual(-5);
254+
expect(parseInt(transform.slice(transform.indexOf('(') + 1, transform.indexOf(')')), 10)).toEqual(-5);
254255

255256
spyOn(fix.componentInstance.parentVirtDir.chunkLoad, 'emit');
256257

257258
fix.componentInstance.data = [{ 1: 1, 2: 2, 3: 3, 4: 4 }];
258259
fix.detectChanges();
259260

260-
expect(parseInt(displayContainer.style.top, 10)).toEqual(0);
261+
transform = displayContainer.style.transform;
262+
expect(parseInt(transform.slice(transform.indexOf('(') + 1, transform.indexOf(')')), 10)).toEqual(0);
261263
expect(fix.componentInstance.parentVirtDir.chunkLoad.emit).toHaveBeenCalledTimes(1);
262264
});
263265

projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.ts

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { NgForOfContext } from '@angular/common';
2-
import { ChangeDetectorRef, ComponentRef, Directive, DoCheck, EmbeddedViewRef, EventEmitter, Input, IterableChanges, IterableDiffer, IterableDiffers, NgZone, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, TemplateRef, TrackByFunction, ViewContainerRef, AfterViewInit, booleanAttribute, DOCUMENT, inject } from '@angular/core';
2+
import { ChangeDetectorRef, ComponentRef, Directive, EmbeddedViewRef, EventEmitter, Input, IterableChanges, IterableDiffer, IterableDiffers, NgZone, OnChanges, OnDestroy, OnInit, Output, SimpleChanges, TemplateRef, TrackByFunction, ViewContainerRef, AfterViewInit, booleanAttribute, DOCUMENT, inject, afterNextRender, runInInjectionContext, EnvironmentInjector } from '@angular/core';
33

44
import { DisplayContainerComponent } from './display.container';
55
import { HVirtualHelperComponent } from './horizontal.virtual.helper.component';
@@ -84,16 +84,17 @@ export abstract class IgxForOfToken<T, U extends T[] = T[]> {
8484
],
8585
standalone: true
8686
})
87-
export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U> implements OnInit, OnChanges, DoCheck, OnDestroy, AfterViewInit {
87+
export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U> implements OnInit, OnChanges, OnDestroy, AfterViewInit {
8888
private _viewContainer = inject(ViewContainerRef);
8989
protected _template = inject<TemplateRef<NgForOfContext<T>>>(TemplateRef);
9090
protected _differs = inject(IterableDiffers);
91+
protected _injector = inject(EnvironmentInjector);
9192
public cdr = inject(ChangeDetectorRef);
9293
protected _zone = inject(NgZone);
9394
protected syncScrollService = inject(IgxForOfScrollSyncService);
9495
protected platformUtil = inject(PlatformUtil);
9596
protected document = inject(DOCUMENT);
96-
97+
private _igxForOf: U & T[] | null = null;
9798

9899
/**
99100
* Sets the data to be rendered.
@@ -102,7 +103,16 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
102103
* ```
103104
*/
104105
@Input()
105-
public igxForOf: U & T[] | null;
106+
public get igxForOf(): U & T[] | null {
107+
return this._igxForOf;
108+
}
109+
110+
public set igxForOf(value: U & T[] | null) {
111+
this._igxForOf = value;
112+
if(this._differ) {
113+
this.resolveDataDiff();
114+
}
115+
}
106116

107117
/**
108118
* Sets the property name from which to read the size in the data object.
@@ -444,7 +454,7 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
444454
}
445455
this._maxSize = this._calcMaxBrowserSize();
446456
if (this.igxForScrollOrientation === 'vertical') {
447-
this.dc.instance._viewContainer.element.nativeElement.style.top = '0px';
457+
this.dc.instance._viewContainer.element.nativeElement.style.transform = `translateY(0px)`;
448458
this.scrollComponent = this.syncScrollService.getScrollMaster(this.igxForScrollOrientation);
449459
if (!this.scrollComponent || this.scrollComponent.destroyed) {
450460
this.scrollComponent = vc.createComponent(VirtualHelperComponent).instance;
@@ -484,6 +494,8 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
484494
}
485495
this._updateScrollOffset();
486496
}
497+
this._differ = this._differs.find(this.igxForOf || []).create(this.igxForTrackBy);
498+
this.resolveDataDiff();
487499
}
488500

489501
public ngAfterViewInit(): void {
@@ -555,7 +567,7 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
555567
/**
556568
* @hidden
557569
*/
558-
public ngDoCheck(): void {
570+
public resolveDataDiff(): void {
559571
if (this._differ) {
560572
const changes = this._differ.diff(this.igxForOf);
561573
if (changes) {
@@ -618,7 +630,13 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
618630
// Actual scroll delta that was added is smaller than 1 and onScroll handler doesn't trigger when scrolling < 1px
619631
const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition);
620632
// scrollOffset = scrollOffset !== parseInt(this.igxForItemSize, 10) ? scrollOffset : 0;
621-
this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
633+
runInInjectionContext(this._injector, () => {
634+
afterNextRender({
635+
write: () => {
636+
this.dc.instance._viewContainer.element.nativeElement.style.transform = `translateY(${-scrollOffset}px)`;
637+
}
638+
});
639+
});
622640
}
623641

624642
const maxRealScrollTop = this.scrollComponent.nativeElement.scrollHeight - containerSize;
@@ -903,15 +921,22 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
903921
if (!parseInt(this.scrollComponent.nativeElement.style.height, 10)) {
904922
return;
905923
}
924+
this.scrollComponent.scrollAmount = event.target.scrollTop;
906925
if (!this._bScrollInternal) {
907-
this._calcVirtualScrollPosition(event.target.scrollTop);
926+
this._calcVirtualScrollPosition(this.scrollComponent.scrollAmount);
908927
} else {
909928
this._bScrollInternal = false;
910929
}
911930
const prevStartIndex = this.state.startIndex;
912931
const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition);
913932

914-
this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
933+
runInInjectionContext(this._injector, () => {
934+
afterNextRender({
935+
write: () => {
936+
this.dc.instance._viewContainer.element.nativeElement.style.transform = `translateY(${-scrollOffset}px)`;
937+
}
938+
});
939+
});
915940

916941
this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
917942

@@ -1091,13 +1116,14 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
10911116
if (!parseInt(firstScrollChild.style.width, 10)) {
10921117
return;
10931118
}
1119+
this.scrollComponent.scrollAmount = event.target.scrollLeft;
10941120
if (!this._bScrollInternal) {
1095-
this._calcVirtualScrollPosition(event.target.scrollLeft);
1121+
this._calcVirtualScrollPosition(this.scrollComponent.scrollAmount);
10961122
} else {
10971123
this._bScrollInternal = false;
10981124
}
10991125
const prevStartIndex = this.state.startIndex;
1100-
const scrLeft = event.target.scrollLeft;
1126+
const scrLeft = this.scrollComponent.scrollAmount;
11011127
// Updating horizontal chunks
11021128
const scrollOffset = this.fixedUpdateAllElements(Math.abs(this._virtScrollPosition));
11031129
if (scrLeft < 0) {
@@ -1463,8 +1489,10 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
14631489
const scroll = this.scrollComponent.nativeElement;
14641490
scrollOffset = scroll && this.scrollComponent.size ?
14651491
currentScroll - this.sizesCache[this.state.startIndex] : 0;
1466-
const dir = this.igxForScrollOrientation === 'horizontal' ? 'left' : 'top';
1467-
this.dc.instance._viewContainer.element.nativeElement.style[dir] = -(scrollOffset) + 'px';
1492+
const dir = this.igxForScrollOrientation === 'horizontal' ? 'left' : 'transform';
1493+
this.dc.instance._viewContainer.element.nativeElement.style[dir] = this.igxForScrollOrientation === 'horizontal' ?
1494+
-(scrollOffset) + 'px' :
1495+
`translateY(${-scrollOffset}px)`;
14681496
}
14691497

14701498
protected _adjustScrollPositionAfterSizeChange(sizeDiff) {
@@ -1474,7 +1502,7 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
14741502
this.recalcUpdateSizes();
14751503
const offset = this.igxForScrollOrientation === 'horizontal' ?
14761504
parseInt(this.dc.instance._viewContainer.element.nativeElement.style.left, 10) :
1477-
parseInt(this.dc.instance._viewContainer.element.nativeElement.style.top, 10);
1505+
Number(this.dc.instance._viewContainer.element.nativeElement.style.transform?.match(/translateY\((-?\d+\.?\d*)px\)/)?.[1]);
14781506
const newSize = this.sizesCache[this.state.startIndex] - offset;
14791507
this.scrollPosition = newSize;
14801508
if (this.scrollPosition !== newSize) {
@@ -1526,7 +1554,7 @@ export class IgxGridForOfContext<T, U extends T[] = T[]> extends IgxForOfContext
15261554
selector: '[igxGridFor][igxGridForOf]',
15271555
standalone: true
15281556
})
1529-
export class IgxGridForOfDirective<T, U extends T[] = T[]> extends IgxForOfDirective<T, U> implements OnInit, OnChanges, DoCheck {
1557+
export class IgxGridForOfDirective<T, U extends T[] = T[]> extends IgxForOfDirective<T, U> implements OnInit, OnChanges {
15301558
protected syncService = inject(IgxForOfSyncService);
15311559

15321560
@Input()
@@ -1643,7 +1671,7 @@ export class IgxGridForOfDirective<T, U extends T[] = T[]> extends IgxForOfDirec
16431671
this.syncService.setMaster(this, true);
16441672
}
16451673

1646-
public override ngDoCheck() {
1674+
public override resolveDataDiff() {
16471675
if (this._differ) {
16481676
const changes = this._differ.diff(this.igxForOf);
16491677
if (changes) {
@@ -1677,19 +1705,25 @@ export class IgxGridForOfDirective<T, U extends T[] = T[]> extends IgxForOfDirec
16771705
}
16781706

16791707
public override onScroll(event) {
1680-
if (!parseInt(this.scrollComponent.nativeElement.style.height, 10)) {
1708+
this.scrollComponent.scrollAmount = event.target.scrollTop;
1709+
if (!this.scrollComponent.size) {
16811710
return;
16821711
}
16831712
if (!this._bScrollInternal) {
1684-
this._calcVirtualScrollPosition(event.target.scrollTop);
1713+
this._calcVirtualScrollPosition(this.scrollComponent.scrollAmount);
16851714
} else {
16861715
this._bScrollInternal = false;
16871716
}
16881717
const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition);
1718+
runInInjectionContext(this._injector, () => {
1719+
afterNextRender({
1720+
write: () => {
1721+
this.dc.instance._viewContainer.element.nativeElement.style.transform = `translateY(${-scrollOffset}px)`;
1722+
this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
1723+
}
1724+
});
1725+
});
16891726

1690-
this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
1691-
1692-
this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
16931727
this.cdr.markForCheck();
16941728
}
16951729

@@ -1699,6 +1733,7 @@ export class IgxGridForOfDirective<T, U extends T[] = T[]> extends IgxForOfDirec
16991733
if (!this.scrollComponent || !parseInt(firstScrollChild.style.width, 10)) {
17001734
return;
17011735
}
1736+
this.scrollComponent.scrollAmount = scrollAmount;
17021737
// Updating horizontal chunks
17031738
const scrollOffset = this.fixedUpdateAllElements(Math.abs(scrollAmount));
17041739
if (scrollAmount < 0) {

projects/igniteui-angular/grids/core/src/api.service.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ export class GridBaseAPIService<T extends GridType> implements GridServiceType {
319319
grid.transactions.add(transaction);
320320
} else {
321321
grid.data.push(rowData);
322+
grid.data = cloneArray(grid.data);
322323
}
323324
grid.validation.markAsTouched(rowId);
324325
grid.validation.update(rowId, rowData);
@@ -334,6 +335,7 @@ export class GridBaseAPIService<T extends GridType> implements GridServiceType {
334335
grid.transactions.add(transaction, grid.data[index]);
335336
} else {
336337
grid.data.splice(index, 1);
338+
grid.data = cloneArray(grid.data);
337339
}
338340
} else {
339341
const state: State = grid.transactions.getState(rowID);

0 commit comments

Comments
 (0)