Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
66afb8a
fix(perf): Apply some performance optimizations.
Jan 8, 2026
3272733
chore(*): MInor tweaks to scroll handler.
Jan 9, 2026
62cd4e0
chore(*): Minor tweak for verticalScrollHandler.
Jan 9, 2026
c4119a0
chore(*): Add style writes in afterNextRender's write callback.
Jan 12, 2026
751d526
chore(*): Clean performance observer and custom styles.
Jan 12, 2026
eb38e63
chore(*): Add btn to scroll 100 times in the grids and measure long t…
Jan 12, 2026
afaa5d7
chore(*): Update a few places where top offset is still used.
Jan 13, 2026
8be5b52
chore(*): Fix kbNav that still used top offset.
Jan 14, 2026
2b1c08d
chore(*): Run initial check on the data on init.
Jan 14, 2026
ad36859
chore(*): Update test with new check for transform.
Jan 14, 2026
6cb59bd
chore(*): Update debounce times and top offset checks in tests.
Jan 16, 2026
f2cf55a
chore(*): Set and cache scrollAmount on hscroll too.
Jan 16, 2026
61b12c2
chore(*): Cache scrollAmount in base onScroll too.
Jan 16, 2026
96c7e5e
chore(*): Debounce a bit more for scroll related operations due to th…
Jan 16, 2026
b8f2889
chore(*): Pass new data ref on delete.
Jan 19, 2026
fc7fb34
chore(*): Recalc sizes on scroll due to variable sizes.
Jan 19, 2026
4fdb6e5
chore(*): Add debounce for kbnav.
Jan 19, 2026
d87fa5d
chore(*): Set amount in GridForOf too.
Jan 19, 2026
a3fcc35
chore(*): Trigger change on records added.
Jan 19, 2026
3c446e8
chore(*): Make sure diff inits and fix some timing on tests.
Jan 19, 2026
92e3c37
chore(*): Fix timing in tests.
Jan 19, 2026
db7bfb7
chore(*): Bump timing if tests a bit more.
Jan 19, 2026
75fe919
chore(*): Add a bit more time in loops that navigate.
Jan 19, 2026
571a9d6
chore(*): Document breaking change and comment out related tests.
Jan 21, 2026
fd21b71
chore(*): Bump up debounce time to 60ms so tests do not flicker.
Jan 21, 2026
2a4b72d
chore(*): Disable throttle for tests to make them more stable.
Jan 21, 2026
df72717
chore(*): Add a bit throttle for a few tests that have resize observe…
Jan 21, 2026
ae89901
chore(*): Add to throttle 0 other tests as well.
Jan 21, 2026
3d9b892
chore(*): Add 0 throttle to selection since there's range select with…
Jan 21, 2026
2a143a9
chore(*): More of SCROLL_THROTTLE_TIME 0.
Jan 21, 2026
95da8e5
chore(*): Prevent resize observer error.
Jan 21, 2026
fd25a13
chore(*): Add SCROLL_THROTTLE_TIME 0 for more grid tests.
Jan 21, 2026
65eabef
Merge branch 'master' into mkirova/performance-optimizations
rkaraivanov Jan 22, 2026
7f5bcb4
Merge branch 'master' into mkirova/performance-optimizations
rkaraivanov Jan 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

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

## 21.2.0

### Breaking Changes

- `igxForOf`, `igxGrid`, `igxTreeGrid`, `igxHierarchicalGrid`, `igxPivotGrid`
- 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.

## 21.1.0

### New Features
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
{{ route.title }}
</button>
}

<button igxButton="contained" (click)="OnPerfTest()">
Test scroll performance
</button>
</div>
<router-outlet />
</main>
22 changes: 21 additions & 1 deletion projects/igniteui-angular-performance/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component } from '@angular/core';
import { Component, ViewChild } from '@angular/core';
import { RouterLink, RouterOutlet, Routes } from '@angular/router';
import { IgxButtonDirective } from 'igniteui-angular';
import { routes } from './app.routes';
Expand All @@ -11,4 +11,24 @@ import { routes } from './app.routes';
})
export class AppComponent {
protected routes: Routes = routes;

@ViewChild(RouterOutlet) outlet!: RouterOutlet;

public async OnPerfTest() {
const longTask = [];
const observer = new PerformanceObserver((list) => {
longTask.push(...list.getEntries());
});
observer.observe({ entryTypes: ['longtask'] });
const grid = (this.outlet.component as any).grid || (this.outlet.component as any).pivotGrid;
for (let i = 0; i < 100; i++) {
grid.navigateTo(i * 50);
await new Promise(r => setTimeout(r, 50));
}
const sum = longTask.reduce((acc, task) => acc + task.duration, 0);
const avgTime = sum / longTask.length;
console.log('Long Tasks:'+ longTask.length + ", ", 'Average Long Task Time:', avgTime);
observer.disconnect();

}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<div class="grid-wrapper">
<igx-grid

#grid
[data]="data"
[allowFiltering]="true"
[allowFiltering]="false"
[height]="'100%'"
[width]="'100%'"
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,9 @@ export class PivotGridComponent {
sortDirection: SortingDirection.None
},
{
fullDate: false,
fullDate: true,
quarters: true,
months: false,
months: true,
}),
],
values: [
Expand Down
2 changes: 1 addition & 1 deletion projects/igniteui-angular-performance/src/styles.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@use '../../igniteui-angular/src/lib/core/styles/themes' as *;
@use '../../igniteui-angular/core/src/core/styles/themes' as *;
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
@include core();
@include typography(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,6 @@ export class VirtualHelperBaseDirective implements OnDestroy, AfterViewInit {
this._scrollNativeSize = this.calculateScrollNativeSize();
}

@HostListener('scroll', ['$event'])
public onScroll(event) {
this.scrollAmount = event.target.scrollTop || event.target.scrollLeft;
}


public ngAfterViewInit() {
this._afterViewInit = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,15 +249,17 @@ describe('IgxForOf directive -', () => {
it('should update vertical scroll offsets if igxForOf changes. ', () => {
fix.componentInstance.scrollTop(5);
fix.detectChanges();
let transform = displayContainer.style.transform;

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

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

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

expect(parseInt(displayContainer.style.top, 10)).toEqual(0);
transform = displayContainer.style.transform;
expect(parseInt(transform.slice(transform.indexOf('(') + 1, transform.indexOf(')')), 10)).toEqual(0);
expect(fix.componentInstance.parentVirtDir.chunkLoad.emit).toHaveBeenCalledTimes(1);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NgForOfContext } from '@angular/common';
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';
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';

import { DisplayContainerComponent } from './display.container';
import { HVirtualHelperComponent } from './horizontal.virtual.helper.component';
Expand Down Expand Up @@ -84,16 +84,17 @@ export abstract class IgxForOfToken<T, U extends T[] = T[]> {
],
standalone: true
})
export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U> implements OnInit, OnChanges, DoCheck, OnDestroy, AfterViewInit {
export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U> implements OnInit, OnChanges, OnDestroy, AfterViewInit {
private _viewContainer = inject(ViewContainerRef);
protected _template = inject<TemplateRef<NgForOfContext<T>>>(TemplateRef);
protected _differs = inject(IterableDiffers);
protected _injector = inject(EnvironmentInjector);
public cdr = inject(ChangeDetectorRef);
protected _zone = inject(NgZone);
protected syncScrollService = inject(IgxForOfScrollSyncService);
protected platformUtil = inject(PlatformUtil);
protected document = inject(DOCUMENT);

private _igxForOf: U & T[] | null = null;

/**
* Sets the data to be rendered.
Expand All @@ -102,7 +103,16 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
* ```
*/
@Input()
public igxForOf: U & T[] | null;
public get igxForOf(): U & T[] | null {
return this._igxForOf;
}

public set igxForOf(value: U & T[] | null) {
this._igxForOf = value;
if(this._differ) {
this.resolveDataDiff();
}
}

/**
* Sets the property name from which to read the size in the data object.
Expand Down Expand Up @@ -444,7 +454,7 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
}
this._maxSize = this._calcMaxBrowserSize();
if (this.igxForScrollOrientation === 'vertical') {
this.dc.instance._viewContainer.element.nativeElement.style.top = '0px';
this.dc.instance._viewContainer.element.nativeElement.style.transform = `translateY(0px)`;
this.scrollComponent = this.syncScrollService.getScrollMaster(this.igxForScrollOrientation);
if (!this.scrollComponent || this.scrollComponent.destroyed) {
this.scrollComponent = vc.createComponent(VirtualHelperComponent).instance;
Expand Down Expand Up @@ -484,6 +494,8 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
}
this._updateScrollOffset();
}
this._differ = this._differs.find(this.igxForOf || []).create(this.igxForTrackBy);
this.resolveDataDiff();
}

public ngAfterViewInit(): void {
Expand Down Expand Up @@ -555,7 +567,7 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
/**
* @hidden
*/
public ngDoCheck(): void {
public resolveDataDiff(): void {
if (this._differ) {
const changes = this._differ.diff(this.igxForOf);
if (changes) {
Expand Down Expand Up @@ -618,7 +630,13 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
// Actual scroll delta that was added is smaller than 1 and onScroll handler doesn't trigger when scrolling < 1px
const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition);
// scrollOffset = scrollOffset !== parseInt(this.igxForItemSize, 10) ? scrollOffset : 0;
this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
runInInjectionContext(this._injector, () => {
afterNextRender({
write: () => {
this.dc.instance._viewContainer.element.nativeElement.style.transform = `translateY(${-scrollOffset}px)`;
}
});
});
}

const maxRealScrollTop = this.scrollComponent.nativeElement.scrollHeight - containerSize;
Expand Down Expand Up @@ -903,15 +921,22 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
if (!parseInt(this.scrollComponent.nativeElement.style.height, 10)) {
return;
}
this.scrollComponent.scrollAmount = event.target.scrollTop;
if (!this._bScrollInternal) {
this._calcVirtualScrollPosition(event.target.scrollTop);
this._calcVirtualScrollPosition(this.scrollComponent.scrollAmount);
} else {
this._bScrollInternal = false;
}
const prevStartIndex = this.state.startIndex;
const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition);

this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';
runInInjectionContext(this._injector, () => {
afterNextRender({
write: () => {
this.dc.instance._viewContainer.element.nativeElement.style.transform = `translateY(${-scrollOffset}px)`;
}
});
});

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

Expand Down Expand Up @@ -1091,13 +1116,14 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
if (!parseInt(firstScrollChild.style.width, 10)) {
return;
}
this.scrollComponent.scrollAmount = event.target.scrollLeft;
if (!this._bScrollInternal) {
this._calcVirtualScrollPosition(event.target.scrollLeft);
this._calcVirtualScrollPosition(this.scrollComponent.scrollAmount);
} else {
this._bScrollInternal = false;
}
const prevStartIndex = this.state.startIndex;
const scrLeft = event.target.scrollLeft;
const scrLeft = this.scrollComponent.scrollAmount;
// Updating horizontal chunks
const scrollOffset = this.fixedUpdateAllElements(Math.abs(this._virtScrollPosition));
if (scrLeft < 0) {
Expand Down Expand Up @@ -1463,8 +1489,10 @@ export class IgxForOfDirective<T, U extends T[] = T[]> extends IgxForOfToken<T,U
const scroll = this.scrollComponent.nativeElement;
scrollOffset = scroll && this.scrollComponent.size ?
currentScroll - this.sizesCache[this.state.startIndex] : 0;
const dir = this.igxForScrollOrientation === 'horizontal' ? 'left' : 'top';
this.dc.instance._viewContainer.element.nativeElement.style[dir] = -(scrollOffset) + 'px';
const dir = this.igxForScrollOrientation === 'horizontal' ? 'left' : 'transform';
this.dc.instance._viewContainer.element.nativeElement.style[dir] = this.igxForScrollOrientation === 'horizontal' ?
-(scrollOffset) + 'px' :
`translateY(${-scrollOffset}px)`;
}

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

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

public override ngDoCheck() {
public override resolveDataDiff() {
if (this._differ) {
const changes = this._differ.diff(this.igxForOf);
if (changes) {
Expand Down Expand Up @@ -1677,19 +1705,25 @@ export class IgxGridForOfDirective<T, U extends T[] = T[]> extends IgxForOfDirec
}

public override onScroll(event) {
if (!parseInt(this.scrollComponent.nativeElement.style.height, 10)) {
this.scrollComponent.scrollAmount = event.target.scrollTop;
if (!this.scrollComponent.size) {
return;
}
if (!this._bScrollInternal) {
this._calcVirtualScrollPosition(event.target.scrollTop);
this._calcVirtualScrollPosition(this.scrollComponent.scrollAmount);
} else {
this._bScrollInternal = false;
}
const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition);
runInInjectionContext(this._injector, () => {
afterNextRender({
write: () => {
this.dc.instance._viewContainer.element.nativeElement.style.transform = `translateY(${-scrollOffset}px)`;
this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
}
});
});

this.dc.instance._viewContainer.element.nativeElement.style.top = -(scrollOffset) + 'px';

this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this));
this.cdr.markForCheck();
}

Expand All @@ -1699,6 +1733,7 @@ export class IgxGridForOfDirective<T, U extends T[] = T[]> extends IgxForOfDirec
if (!this.scrollComponent || !parseInt(firstScrollChild.style.width, 10)) {
return;
}
this.scrollComponent.scrollAmount = scrollAmount;
// Updating horizontal chunks
const scrollOffset = this.fixedUpdateAllElements(Math.abs(scrollAmount));
if (scrollAmount < 0) {
Expand Down
2 changes: 2 additions & 0 deletions projects/igniteui-angular/grids/core/src/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,7 @@ export class GridBaseAPIService<T extends GridType> implements GridServiceType {
grid.transactions.add(transaction);
} else {
grid.data.push(rowData);
grid.data = cloneArray(grid.data);
}
grid.validation.markAsTouched(rowId);
grid.validation.update(rowId, rowData);
Expand All @@ -334,6 +335,7 @@ export class GridBaseAPIService<T extends GridType> implements GridServiceType {
grid.transactions.add(transaction, grid.data[index]);
} else {
grid.data.splice(index, 1);
grid.data = cloneArray(grid.data);
}
} else {
const state: State = grid.transactions.getState(rowID);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { SampleTestData } from '../../../../test-utils/sample-test-data.spec';
import { IgxActionStripComponent } from 'igniteui-angular/action-strip';
import { IgxGridComponent } from 'igniteui-angular/grids/grid';


const DEBOUNCETIME = 60;
describe('igxGridPinningActions #grid ', () => {
let fixture;
let actionStrip: IgxActionStripComponent;
Expand Down Expand Up @@ -65,7 +65,7 @@ describe('igxGridPinningActions #grid ', () => {
jumpButton.triggerEventHandler('click', new Event('click'));
await wait();
fixture.detectChanges();
await wait();
await wait(DEBOUNCETIME);
fixture.detectChanges();

const secondToLastVisible = grid.rowList.toArray()[grid.rowList.length - 2];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,8 @@ export class IgxGridNavigationService {
return Math.ceil(this.grid.headerContainer.scrollPosition);
}
public get containerTopOffset() {
return parseInt(this.grid.verticalScrollContainer.dc.instance._viewContainer.element.nativeElement.style.top, 10);
const transform = this.grid.verticalScrollContainer.dc.instance._viewContainer.element.nativeElement.style.transform
return Number(transform.match(/translateY\((-?\d+\.?\d*)px\)/)?.[1])
}

protected getColumnUnpinnedIndex(visibleColumnIndex: number) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Subject } from 'rxjs';
import { DefaultSortingStrategy, IgxStringFilteringOperand, SortingDirection, TransactionType } from 'igniteui-angular/core';
import { IgxGridMRLNavigationService } from 'igniteui-angular/grids/core';

const DEBOUNCETIME = 30;
const DEBOUNCETIME = 60;

describe('IgxGrid - Row Adding #grid', () => {
const GRID_ROW = 'igx-grid-row';
Expand Down
Loading
Loading