Repository navigation
fix(grid): skip redundant sizing pass after data and column change - #17801
viktorkombov wants to merge 16 commits into
Conversation
The post-render width callback in calculateGridSizes re-checks the grid after every sizing pass. When the data and columns of a percentage-height grid are replaced, that re-check already sizes the grid for the new columns, and the Promise-deferred repaints from onColumnsAddedOrRemoved and resetSummaryHeight then ran a third, identical sizing pass. Route those repaints through notifyChangesAfterCurrentPass, which drops the request once a post-render check and a new sizing pass have run after it. Closes #17792 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🔵 Needs a closer look
Lifecycle-sensitive repaint suppression needs runtime validation covering actual column-field additions and removals before approval.
2 open issues
What changed in this PR
Addresses #17792 by suppressing redundant deferred grid repaints while preserving the post-render checks needed for zoneless applications.
Changes:
- Adds shared repaint suppression for column and summary updates.
- Adds zoneless and ZoneJS sizing and geometry tests.
- Documents the performance fix.
| File | Description |
|---|---|
projects/igniteui-angular/grids/grid/src/grid.component.spec.ts |
Adds sizing and rendering regression tests. |
projects/igniteui-angular/grids/grid/src/grid-base.directive.ts |
Tracks render checks to skip redundant repaints. |
projects/igniteui-angular/grids/core/src/summaries/grid-summary.service.ts |
Routes summary repaints through the new helper. |
projects/igniteui-angular/grids/core/src/common/grid.interface.ts |
Declares the shared notification method. |
CHANGELOG.md |
Records the sizing performance fix. |
Review effort: Balanced
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
22.2.3 is released, so the entry moves to Unreleased and is narrowed to replacing columns that keep their fields, the case with the extra pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The hierarchical grid publishes new columns in a Promise and re-sizes only when their count changes. When a sizing pass ran before the publication, the deferred summary repaint was dropped, so the old headers and widths stayed until the next change detection. Request a repaint after a same-count replacement as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In the data switch of #17792 the new data meets the old filters, so the vertical scrollbar hides and shows again, and that repaint request outlives the one the removed columns cancel. Filter an old column in the new-fields case, so it fails without the fix, and restore the general changelog entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts: # CHANGELOG.md # projects/igniteui-angular/grids/grid/src/grid.component.spec.ts # projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts
# Conflicts: # CHANGELOG.md # projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts
mddragnev
left a comment
There was a problem hiding this comment.
There are 17 new tests for a perfomance regression? Please clean that up. We might need just a couple
Replace notifyChangesAfterCurrentPass() and its counters with the approach suggested in review: - _applyWidthHostBinding() marks the host for check only when the host width changes, so the post-render check no longer re-runs ngDoCheck and a sizing pass in the same tick. - If a repaint is still pending after the post-render check, request it again in a microtask, so it runs in one pass with the deferred column and summary repaints. A private flag keeps a deferred plain notifyChanges(), such as the one from the isLoading setter, from cancelling it. - IgxHierarchicalGridComponent.updateColumnList() requests a repaint for every root column replacement, because it applies the new columns in a Promise, outside the parent's check. This also fixes same-count replacement with summaryCalculationMode set to childLevelsOnly. - Remove notifyChangesAfterCurrentPass() from GridType and restore the summary service's deferred repaint. - Reduce the new tests to three, and compare settled scroll sizes in the group row expand/collapse test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agreed. I kept three, each catching a different wrong fix: the duplicate-pass test, data and column widths changing in the same pass, and an HG root column replacement with |
- Add a test where isLoading turns off in the same pass as a column width change. The isLoading setter defers a plain notifyChanges(), which cancelled the re-requested repaint before the _repaintAfterRender flag. The test fails without the flag. - Remove the width === null branch of expectDynamicColumnsGeometry, which no test uses any more and code scanning flagged as dead code, and drop its width parameter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first commit of this round routed every root column replacement through notifyChanges(true). For a change in the number of columns this moved the sizing from the column microtask to the next change detection, which made the elements spec "should populate hgrid's view initially when there are no layouts" fail when the code runs slower, as it does with coverage. - For a different number of columns, size synchronously with calculateGridSizes(false) again, as before this PR, and then mark the host for check: the pass runs outside the parent's check, so the host bindings, such as aria-colcount, need one. - Keep notifyChanges(true) for replacements with the same number of columns. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep only the comment on the deferred repaint in the post-render check, which the code alone does not explain, and master's comment in the group row expand/collapse test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
f09f1d5 to
f5fe239
Compare
Testing notesThe same steps apply to #17801 (master), #17803 (22.2.x) and #17802 (21.2.x). What this fixes
On screen the grid looks the same as before. Only the number of sizing passes changes, and the sample below counts them. 1. Check the fix
Expected with the PR: Without the PR (the same steps on 2. Regression checks In these dev demos, everything should look and behave as it does without the PR, with no console errors:
grid-data-switch.sample.tsimport { ChangeDetectionStrategy, Component, afterNextRender, signal, viewChild } from '@angular/core';
import { IgxButtonDirective, IgxColumnComponent, IgxGridComponent } from 'igniteui-angular';
const ROWS = 10000;
const COLUMNS = 30;
const makeColumns = (gen: number) => Array.from({ length: COLUMNS }, (_, i) =>
({ key: `g${gen}c${i}`, field: `f${i}`, header: `G${gen} C${i}`, width: `${100 + (gen % 2) * 40}px` }));
const makeData = (gen: number) => Array.from({ length: ROWS }, (_, r) => {
const row: Record<string, unknown> = { id: r };
for (let c = 0; c < COLUMNS; c++) {
row[`f${c}`] = `g${gen} r${r} c${c}`;
}
return row;
});
@Component({
selector: 'app-grid-data-switch-sample',
template: `
<div class="controls" style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px">
<button igxButton="contained" (click)="switchData()">Switch data and columns</button>
<span class="result">{{ result() }}</span>
</div>
<div style="height: 600px">
<igx-grid #grid [data]="data()" width="100%" height="100%" [allowFiltering]="true" [rowSelection]="'multiple'">
@for (col of columns(); track col.key) {
<igx-column [field]="col.field" [header]="col.header" [sortable]="true" [width]="col.width"></igx-column>
}
</igx-grid>
</div>`,
imports: [IgxGridComponent, IgxColumnComponent, IgxButtonDirective],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GridDataSwitchSampleComponent {
public grid = viewChild.required<IgxGridComponent>('grid');
public data = signal(makeData(0));
public columns = signal(makeColumns(0));
public result = signal('');
private gen = 0;
private passes = 0;
constructor() {
// QA only: count the grid's sizing passes (calls to calculateGridSizes) after each switch
afterNextRender(() => {
const grid = this.grid() as any;
const calculateGridSizes = grid.calculateGridSizes;
grid.calculateGridSizes = (...args: unknown[]) => {
this.passes++;
return calculateGridSizes.apply(grid, args);
};
});
}
public switchData() {
this.gen++;
this.passes = 0;
this.result.set('');
this.data.set(makeData(this.gen));
this.columns.set(makeColumns(this.gen));
setTimeout(() => this.result.set(`Sizing passes: ${this.passes}`), 1000);
}
} |

Closes #17792
Description
Replacing the data and the columns of a percentage-height grid ran three sizing passes instead of two. This is the "Data switch" slowdown reported since 21.2.12 / 22.0.4.
Problem: since #17328, the post-render check in
calculateGridSizes()calls_applyWidthHostBinding(), which always marks the host for check. That re-runs the grid'sngDoCheckin the same tick, which sizes the grid for the repaints requested after the firstngDoCheck(the new columns' inputs). The Promise-deferred repaints of the same change, fromonColumnsAddedOrRemoved()andIgxGridSummaryService.resetSummaryHeight(), then run a third pass.Fix:
_applyWidthHostBinding()marks the host only when the width changes.ngDoCheck, for example by column[width]inputs or a toolbar, left a stale layout. A private flag keeps a deferred plainnotifyChanges(), such as the one fromisLoading, from cancelling that request.IgxHierarchicalGridComponent.updateColumnList()applies the new root columns in a Promise, outside the parent's check. It now requests a repaint when they replace columns of the same count, and marks the host for check after sizing a changed count, so host bindings such asaria-colcountstay current. The same-count repaint also fixessummaryCalculationModeset tochildLevelsOnly, which kept the old headers and widths.Motivation / Context
Fixes the regression reported in #17792: the benchmark's data switch runs two sizing passes instead of three. Skipping the post-render callback for explicit widths is not an option: it breaks existing zoneless tests (aria-colcount with grouped columns, last row after scrolling) and two keyboard navigation tests.
Type of Change (check all that apply):
Component(s) / Area(s) Affected:
Grid, Tree Grid, Hierarchical Grid (shared sizing in the grid base)
How Has This Been Tested?
Zoneless specs with exhaustive
checkNoChanges, so a missing notification fails them:grid.component.spec.ts)grid.component.spec.ts)isLoadingturns off together with a column width change, which covers the private flag (grid.component.spec.ts)childLevelsOnly: root columns replaced with the same count, then with fewer columns (hierarchical-grid.spec.ts)should recalculate visible chunk data and scrollbar size when expanding/collapsing group rowsnow compares settled scroll sizes. It compared unmeasured row-height estimates, which the grid now replaces with measured heights a little earlier.The full grids suite passes locally, except for locale-dependent date formatting and PDF export timeouts that fail on master as well.
Test Configuration:
Screenshots / Recordings
N/A
Checklist:
feature/README.MDupdates for the feature docsREADME.MDCHANGELOG.MDupdates for newly added functionalityng updatemigrations for the breaking changes (migrations guidelines)🤖 Generated with Claude Code