Skip to content

perf(grid): fix style recalculation regressions in grid themes and component styles - 22.2.x - #17841

Open
simeonoff wants to merge 7 commits into
22.2.xfrom
simeonoff/perf-fixes-22.2.x
Open

simeonoff wants to merge 7 commits into
22.2.xfrom
simeonoff/perf-fixes-22.2.x

Conversation

@simeonoff

Copy link
Copy Markdown
Member

Closes #17793
Addresses (a) of #17794. Patch of #17839 for 22.2.x.

Description

Both commits are cherry-picked from master with -x. The only conflict was CHANGELOG.md; the entries go under 22.2.3 as with other patches since that tag. The registrar spec is ported from Vitest to Jasmine for this branch.

Motivation / Context

Measured on this branch at 4× CPU throttle, as main-thread time:

Operation Before → after
First filter + sort 132 → 102 ms (−23%)
Data source switch 442 → 396 ms (−10.5%)
Next filter 104 → 74 ms (−29%)
Load −0.9% (not significant)

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 (all variants), Theming, IgxStylesRegistrar

How Has This Been Tested?

  • Unit tests
  • Manual testing
  • Automated e2e tests

All checks below were run on this branch, with its own dependencies (npm ci).

  • Unit tests (Karma): the registrar spec passes 8/8, and removing any piece of the logic makes at least one test fail. Grid filtering (455), group-by (123), editing (144), selection (184), tree grid (522) and hierarchical grid (262) suites pass, as do test:styles and lint:lib.
  • CSS equivalence: all 68 compiled sheets normalize to identical output.
  • Visual parity: computed styles of every element match the base branch across editing, loading, filtering, grouping, the Excel-style menu, and the tree, hierarchical and pivot grids. The only differences are in the circular progress bar: its gradient id and animation phase.
  • Style churn: traced with a MutationObserver on <head>. No retained sheet is added or removed after load.

Test Configuration:

  • Angular version: 22.2.0
  • Browser(s): Chrome Headless 155, Chromium (Playwright)
  • OS: macOS

Screenshots / Recordings

N/A, no visual changes.

Checklist:

  • All relevant labels 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

Chromium (and WebKit) only bucket a rule by a :where()/:is() subject
when it holds a single selector; multi-argument lists land in the
universal bucket and are matched against every element on each style
recalculation. Wrap each selector of the scope() list in its own
:where(), and split the header item rule the same way. Specificity and
matching are unchanged.

Closes #17793

(cherry picked from commit 24ab05e)
Angular removes a ViewEncapsulation.None component's styles with its
last instance and re-inserts them with the next, restyling the page.
Add IgxStylesRegistrar.retain() to keep them via one detached instance
per app, and retain the components the grid creates and destroys while
filtering, grouping, editing and loading.

Refs #17794

(cherry picked from commit f24054a)
@ChronosSF

Copy link
Copy Markdown
Member

@simeonoff I measured and reviewed this branch. The performance fixes hold up, but retain() introduces one regression.

Switching the theme at runtime raises an error once a grid exists

After a grid has been created, every change of the root THEME_TOKEN (e.g. inject(THEME_TOKEN).set('fluent')) raises an uncaught error:

TypeError: Cannot read properties of undefined (reading 'type')
    at IgxInputGroupComponent.get isFileType (input-group.component.ts:393)

Cause:

  • The retained IgxInputGroupComponent (created here) subscribes to THEME_TOKEN in its constructor, and calls this.cdr.detectChanges() when the theme changes (L250-L256).
  • That refreshes its host bindings. isFileType reads this.input.type (L392-L394), but the retained instance never gets a projected igxInput, so input is undefined.

Scope:

  • It happens with and without zone.js. It also continues after the grid is destroyed, since the retained instance lives as long as the application.
  • The base 22.2.x doesn't have it.
  • Visible input groups still switch theme correctly. What apps get is one uncaught error per switch, through RxJS's unhandled-error path: window.onerror, error monitoring, and failing tests in apps' own Jasmine suites, which fail on uncaught errors.
  • perf(grid): fix style recalculation regressions in grid themes and component styles #17839 makes the same retain() call, so master very likely has the same problem. I only ran it on this branch.

Repro (Jasmine, zone or zoneless):

import { config } from 'rxjs';
import { THEME_TOKEN } from 'igniteui-angular/core';

it('switching the theme with a grid on the page raises no errors', async () => {
    const errors: unknown[] = [];
    const previous = config.onUnhandledError;
    config.onUnhandledError = error => errors.push(error);
    try {
        const fixture = TestBed.createComponent(GridHostComponent); // any component with an <igx-grid>
        fixture.detectChanges();
        await fixture.whenStable();
        const token = TestBed.inject(THEME_TOKEN);
        token.set(token.theme === 'fluent' ? 'material' : 'fluent');
        await new Promise(resolve => setTimeout(resolve, 50));
        expect(errors).toEqual([]); // fails on this branch, passes on 22.2.x
    } finally {
        config.onUnhandledError = previous;
    }
});

Suggested fix: make the input group's file-input getters null-safe: this.input?.type === 'file' in isFileType, and the same guard in isFileInput and fileNames. That covers any path that runs change detection on the retained instance, not just the theme change.

  • A narrower alternative is to skip detectChanges() in the theme callback while this.input is undefined.
  • The repro above would make a good regression test.

CHANGELOG wording

The entry says the ids of these components "start from 1 instead of 0 on pages with a grid". Two corrections:

  • Not a fixed start: each application's first grid uses up one auto-generated id of each retained component, and later grids use none. A checkbox created before the grid still gets 0; the ones created after it are one higher than before. In tests, this happens once per TestBed module.
  • The input group: it has no auto-generated id, so it shouldn't be in that list. igx-circular-gradient-N shifts too.

Checked, nothing found

  • Compiled CSS: all 68 sheets built at both commits. After expanding the multi-argument :where() lists, there are no semantic differences, and rule counts and order are identical.
  • Tests on this branch: all passed.
    • test:styles 8/8, the registrar spec 8/8.
    • The retained components' specs: 259 passed, 1 skipped.
    • Grid filtering, editing and selection suites: 839/839.
    • Pivot grid 223/223, hierarchical grid 283/283, test:elements 22/22.
    • lint:lib: no errors.
  • Lifecycle: the retained instances are destroyed with the application. They attach no window listeners, timers or focus handling, and Angular Elements and SSR render without errors.
  • Worth knowing: server-rendered pages with a grid now inline about 160 KB more CSS. A plain grid page went from 120 KB to 274 KB of HTML.

Performance, measured independently

I built this branch and its base from source and ran them interleaved on 3 CI runners per row count (wall time, no CPU throttling). Changes against the base:

Scenario 10k rows 100k rows
Filter + sort −50 to −54% −18 to −20%
Data switch −11 to −13% −5 to −7%
Scroll −4 to −7% −4 to −8%, only one runner certain
Select all −4 to −6% too noisy to judge
Loading unchanged unchanged

Filter + sort and data switch are back at 22.0.8's level at both row counts. Loading stays about 6% above 22.0.8, which is the part of #17794 this PR doesn't cover.

@ChronosSF ChronosSF added 🛠️ status: in-development Issues and PRs with active development on them and removed ❌ status: awaiting-test PRs awaiting manual verification labels Oct 26, 2026
@ChronosSF ChronosSF assigned simeonoff and unassigned ChronosSF Oct 26, 2026
The detached input group kept alive by IgxStylesRegistrar.retain() has no
projected igxInput, so a runtime THEME_TOKEN change ran detectChanges() on
it and threw from isFileType. Make the getters null-safe.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants