Skip to content

fix(grid): skip redundant sizing pass after data and column change - #17801

Open
viktorkombov wants to merge 16 commits into
masterfrom
vkombov/fix-17792
Open

viktorkombov wants to merge 16 commits into
masterfrom
vkombov/fix-17792

Conversation

@viktorkombov

@viktorkombov viktorkombov commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

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's ngDoCheck in the same tick, which sizes the grid for the repaints requested after the first ngDoCheck (the new columns' inputs). The Promise-deferred repaints of the same change, from onColumnsAddedOrRemoved() and IgxGridSummaryService.resetSummaryHeight(), then run a third pass.

Fix:

  • _applyWidthHostBinding() marks the host only when the width changes.
  • If a repaint is still pending after the post-render check, the grid requests it again in a microtask, so it runs in one pass with the deferred repaints. Without this, repaints requested after ngDoCheck, for example by column [width] inputs or a toolbar, left a stale layout. A private flag keeps a deferred plain notifyChanges(), such as the one from isLoading, 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 as aria-colcount stay current. The same-count repaint also fixes summaryCalculationMode set to childLevelsOnly, 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):

  • Bug fix
  • New functionality
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Refactoring (no functional changes)
  • Documentation
  • Demos
  • CI/CD
  • Tests
  • Changelog
  • Skills/Agents

Component(s) / Area(s) Affected:

Grid, Tree Grid, Hierarchical Grid (shared sizing in the grid base)

How Has This Been Tested?

  • Unit tests
  • Manual testing
  • Automated e2e tests

Zoneless specs with exhaustive checkNoChanges, so a missing notification fails them:

  • no duplicate sizing pass when the data and columns of a percentage-height grid are replaced, with the same fields and with new fields after filtering an old column, as in the benchmark (grid.component.spec.ts)
  • the grid is sized when its data and column widths change in the same pass (grid.component.spec.ts)
  • the grid is sized when isLoading turns off together with a column width change, which covers the private flag (grid.component.spec.ts)
  • hierarchical grid with 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 rows now 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:

  • Angular version: 22.2.3
  • Browser(s): Chromium (Vitest browser mode)
  • OS: Windows 11

Screenshots / Recordings

N/A

Checklist:

  • All relevant tags have been applied to this PR
  • This PR includes unit tests covering all the new code (test guidelines)
  • This PR includes API docs for newly added methods/properties (api docs guidelines)
  • This PR includes feature/README.MD updates for the feature docs
  • This PR includes general feature table updates in the root README.MD
  • This PR includes CHANGELOG.MD updates for newly added functionality
  • This PR contains breaking changes
  • This PR includes ng update migrations for the breaking changes (migrations guidelines)
  • This PR includes behavioral changes and the feature specification has been updated with them
  • Accessibility (ARIA, keyboard navigation, focus management) has been verified

🤖 Generated with Claude Code

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>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 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

Comment thread CHANGELOG.md Resolved
viktorkombov and others added 5 commits October 19, 2026 22:38
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
@ChronosSF
ChronosSF requested a review from rkaraivanov October 23, 2026 14:23
viktorkombov and others added 2 commits October 26, 2026 12:34
# Conflicts:
#	CHANGELOG.md
#	projects/igniteui-angular/grids/hierarchical-grid/src/hierarchical-grid.spec.ts
@ChronosSF
ChronosSF requested a review from mddragnev October 30, 2026 12:08

@mddragnev mddragnev left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 17 new tests for a perfomance regression? Please clean that up. We might need just a couple

viktorkombov and others added 2 commits November 3, 2026 15:29
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>
@viktorkombov

viktorkombov commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor Author

There are 17 new tests for a perfomance regression? Please clean that up. We might need just a couple

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 childLevelsOnly. The other 14 never failed differently from these. I also added one for isLoading turning off together with a column width change, which covers the new flag.

- 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>
viktorkombov and others added 2 commits November 4, 2026 11:36
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>
@viktorkombov

Copy link
Copy Markdown
Contributor Author

Testing notes

The same steps apply to #17801 (master), #17803 (22.2.x) and #17802 (21.2.x).

What this fixes

  • The performance regression from Performance regression: grid data-source switch 14–17% slower since 21.2.12 / 22.0.4 (extra sizing pass) #17792. Since 21.2.12 and 22.0.4, replacing a grid's data and columns runs three sizing passes instead of two. This made the data-source switch in the grid benchmark 14–17% slower. The measurements and the cause are in the issue. With this change the grid runs two passes again, as in 21.2.11 and 22.0.3.
  • Stale hierarchical grid columns. With summaryCalculationMode set to childLevelsOnly, replacing the root columns with the same number of columns kept the old headers and widths on screen. A new test in hierarchical-grid.spec.ts covers this. There is no dev demo for it.

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

  1. Check out the PR branch and run npm ci.
  2. Save the sample below as src/app/grid-data-switch/grid-data-switch.sample.ts, and add it to src/app/app.routes.ts:
    import { GridDataSwitchSampleComponent } from './grid-data-switch/grid-data-switch.sample';
    // in the routes array:
    { path: 'gridDataSwitch', component: GridDataSwitchSampleComponent },
  3. Run npm start and open http://localhost:4200/gridDataSwitch.
  4. Click Switch data and columns a few times. The count appears next to the button after a second.

Expected with the PR: Sizing passes: 2 on every click. The headers change to G1 C0, G2 C0, …, the cells to g1 r0 c0, g2 r0 c0, …, and the column width alternates between 140px and 100px. No errors in the console.

Without the PR (the same steps on master, 22.2.x or 21.2.x), the grid shows Sizing passes: 3 and the same layout.

2. Regression checks

In these dev demos, everything should look and behave as it does without the PR, with no console errors:

  • gridPercentage: resize the browser window. The grid and its percentage-width columns follow.
  • gridColumnResizing: resize a column by dragging it, click AutoSize API, and pin and unpin columns.
  • gridToolbar: hide, show, pin and unpin columns from the toolbar.
  • gridGroupBy: group and ungroup, collapse and expand groups, and scroll to the last row.
  • gridMasterDetail: click Expand All, then press Ctrl+End in a cell. The last row is fully visible.
  • hierarchicalGrid: click Enable Summaries and Set rowSelection via binding, and expand child grids.
grid-data-switch.sample.ts
import { 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);
    }
}

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

grid ❌ status: awaiting-test PRs awaiting manual verification

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Performance regression: grid data-source switch 14–17% slower since 21.2.12 / 22.0.4 (extra sizing pass)

3 participants