From cf30c129f18002b0c2ae5e181aca8918eacf6346 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 2 May 2026 17:58:59 +0200 Subject: [PATCH] perf(ui): lazy-load content-card images + OnPush on sources filters panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small focused changes from an Angular perf audit. content-card lazy loading ContentCard is the building block for catalog grids and rails — Xtream recently-added (3 rails × 20 = 60 cards on first paint), Xtream/Stalker search results, unified favorites/recent grids. The poster had no loading attribute, so every off-screen card eagerly fetched its poster from the Xtream/Stalker server on initial render. Same fix already in place on the dashboard rail's separate ; mirror it on the shared ContentCard so every consumer benefits. Add loading="lazy" decoding="async" to both tags (real poster and the default-poster fallback). Browser defers off-screen requests; no JS changes. OnPush on workspace-sources-filters-panel The sources filters panel renders one row per filter option and displays {{ getTypeCount(option.id) }} per row — that method is a plain Map lookup but fires every CD cycle under default change detection. The component is signal-only (computed + selectSignal, no subscribes) so the OnPush conversion is purely additive. Co-Authored-By: Claude Opus 4.7 (1M context) Entire-Checkpoint: f957cd9849e0 --- .../components/content-card/content-card.component.html | 4 ++++ .../workspace-sources-filters-panel.component.ts | 8 +++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/libs/portal/shared/ui/src/lib/components/content-card/content-card.component.html b/libs/portal/shared/ui/src/lib/components/content-card/content-card.component.html index 4e787fa3b..a89a09cce 100644 --- a/libs/portal/shared/ui/src/lib/components/content-card/content-card.component.html +++ b/libs/portal/shared/ui/src/lib/components/content-card/content-card.component.html @@ -17,6 +17,8 @@ [alt]="title()" (error)="onImageError($event)" class="poster" + loading="lazy" + decoding="async" /> } @else if (showPlaceholder()) {
@@ -27,6 +29,8 @@ src="./assets/images/default-poster.png" [alt]="title()" class="poster" + loading="lazy" + decoding="async" /> } diff --git a/libs/workspace/shell/feature/src/lib/workspace-sources-filters-panel/workspace-sources-filters-panel.component.ts b/libs/workspace/shell/feature/src/lib/workspace-sources-filters-panel/workspace-sources-filters-panel.component.ts index 630a4ffae..97e3a7531 100644 --- a/libs/workspace/shell/feature/src/lib/workspace-sources-filters-panel/workspace-sources-filters-panel.component.ts +++ b/libs/workspace/shell/feature/src/lib/workspace-sources-filters-panel/workspace-sources-filters-panel.component.ts @@ -1,4 +1,9 @@ -import { computed, Component, inject } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + inject, +} from '@angular/core'; import { MatIcon } from '@angular/material/icon'; import { MatListModule } from '@angular/material/list'; import { Store } from '@ngrx/store'; @@ -25,6 +30,7 @@ const ALL_FILTERS = ['m3u', 'xtream', 'stalker']; imports: [MatIcon, MatListModule, TranslatePipe], templateUrl: './workspace-sources-filters-panel.component.html', styleUrl: './workspace-sources-filters-panel.component.scss', + changeDetection: ChangeDetectionStrategy.OnPush, }) export class WorkspaceSourcesFiltersPanelComponent { private readonly store = inject(Store);