perf(ui): make the libs/ui Eager components OnPush (#1818)

* docs(performance): inventory the zoneless change-detection migration

Plan item C6 step 2. docs/architecture/zoneless-migration.md lists the 66
production files (67 components) that still set
ChangeDetectionStrategy.Eager, the ten places where a template-read plain
field is written outside an Angular event, the NgZone and
ChangeDetectorRef calls to remove at the flip, and the IPC, player,
observer, timer and dialog paths checked as signal-safe.

On Angular 22 an unset changeDetection already means OnPush, so only the
explicit Eager components re-render on every tick.

zoneless-migration.spec.ts in the performance harness compares the
checklist with the code: a new Eager component, or a converted one left
unticked, fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(ui): make the libs/ui Eager components OnPush

Plan item C6 step 3 for libs/ui/components and libs/ui/epg: eleven
components (twelve with the EPG trust dialog) set
ChangeDetectionStrategy.Eager and were checked on every tick, among them
the always-mounted EPG progress panel the idle audit found re-rendering
on every idle tick. Their template state is already signals, signal
inputs, immutable dialog data or fields written from template events, so
they switch to OnPush without other changes.

The epg-item-description spec mutated dialog data after creation and
marked only the fixture's host view; it now marks the component's own
view, which OnPush requires. The libs/ui playback and remote-control
components stay Eager for their own PRs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: 4gray <fourgray@proton.me>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-04 16:44:40 +02:00
1 parent bf3cf87b65
commit 26ca8e2cb0
16 files changed
+367 -24

No files matched your search

@@ -29,8 +29,7 @@ export interface ConfirmDialogData {
@Component({
imports: [MatButtonModule, MatDialogModule, TranslateModule],
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h2 mat-dialog-title>
{{ dialogData.title }}
@@ -34,8 +34,7 @@ export type ContentHeroLayout = 'stage' | 'compact';
TranslateModule,
],
templateUrl: './content-hero.component.html',
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['./content-hero.component.scss'],
})
export class ContentHeroComponent {
@@ -47,8 +47,7 @@ import { TranslateModule } from '@ngx-translate/core';
</button>
}
`,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
:host {
@@ -56,8 +56,7 @@ import { TranslateModule } from '@ngx-translate/core';
</div>
</section>
`,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['./content-about.component.scss'],
})
export class ContentAboutComponent {
@@ -43,8 +43,7 @@ import {
imports: [ContentHeroComponent, ContentAboutComponent, NgTemplateOutlet],
templateUrl: './portal-detail-shell.component.html',
styleUrls: ['./portal-detail-shell.component.scss'],
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
tabindex: '0',
role: 'region',
@@ -19,8 +19,7 @@ import {
></div>
</div>
`,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
.progress-capsule {
@@ -105,8 +105,7 @@ export const EPISODE_INFO_PLAY = 'play' as const;
</button>
</div>
`,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
:host {
@@ -12,8 +12,7 @@ import { MatIcon } from '@angular/material/icon';
</div>
}
`,
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
.watched-badge {
@@ -1,4 +1,4 @@
import { signal } from '@angular/core';
import { ChangeDetectorRef, signal } from '@angular/core';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { By } from '@angular/platform-browser';
@@ -72,8 +72,12 @@ describe('EpgItemDescriptionComponent', () => {
});
it('offers archive URL copying only with explicit capability and explains credentials', () => {
// The component is OnPush and dialog data is fixed once the dialog
// opens, so the test marks the component's own view after mutating it.
const componentView =
fixture.debugElement.injector.get(ChangeDetectorRef);
Object.assign(component.dialogData, { archiveUrlAvailable: true });
fixture.changeDetectorRef.markForCheck();
componentView.markForCheck();
fixture.detectChanges();
expect(
fixture.nativeElement.querySelector(
@@ -84,7 +88,7 @@ describe('EpgItemDescriptionComponent', () => {
'EPG.PROGRAM_DIALOG.ARCHIVE_URL_NOTICE'
);
Object.assign(component.dialogData, { archiveUrlAvailable: false });
fixture.changeDetectorRef.markForCheck();
componentView.markForCheck();
fixture.detectChanges();
expect(
fixture.nativeElement.querySelector(
@@ -38,7 +38,7 @@ export type EpgItemDialogData = EpgProgram & {
selector: 'app-epg-item-description',
templateUrl: './epg-item-description.component.html',
styleUrls: ['./epg-item-description.component.scss'],
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [DatePipe, MatDialogModule, MatIcon, TranslatePipe],
})
export class EpgItemDescriptionComponent {
@@ -32,7 +32,7 @@ interface EpgTrustConfirmDialogData {
@Component({
selector: 'app-epg-trust-confirm-dialog',
imports: [MatButtonModule, MatDialogModule, TranslatePipe],
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h2 mat-dialog-title>{{ data.title }}</h2>
<mat-dialog-content class="mat-typography">
@@ -64,7 +64,7 @@ class EpgTrustConfirmDialogComponent {
TranslatePipe,
],
templateUrl: './epg-progress-panel.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './epg-progress-panel.component.scss',
})
export class EpgProgressPanelComponent {
@@ -30,7 +30,7 @@ type BadgeStatus =
TranslatePipe,
],
templateUrl: './epg-source-status.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrl: './epg-source-status.component.scss',
})
export class EpgSourceStatusComponent implements OnInit {