mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 13:16:16 -08:00
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:
16 files changed
+367
-24
No files matched your search
@@ -0,0 +1,91 @@
|
||||
/* eslint-disable playwright/expect-expect -- These are Node assertion-based repository contract tests. */
|
||||
import assert from 'node:assert/strict';
|
||||
import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { join, relative, sep } from 'node:path';
|
||||
import test from 'node:test';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// docs/architecture/zoneless-migration.md lists every component that still
|
||||
// opts out of OnPush. This keeps the checklist and the code in step: a new
|
||||
// Eager component fails here, and so does a converted one left unticked.
|
||||
|
||||
const workspaceRoot = fileURLToPath(new URL('../../../../', import.meta.url));
|
||||
const checklistPath = 'docs/architecture/zoneless-migration.md';
|
||||
const sourceRoots = ['apps', 'libs'];
|
||||
const skippedDirectories = new Set(['node_modules', 'dist', 'coverage']);
|
||||
const testOnlyFile =
|
||||
/(\.spec|\.spec-stubs|\.spec-data|\.test-helpers|test-setup)\.ts$/;
|
||||
|
||||
function listProductionSources(directory: string): string[] {
|
||||
const files: string[] = [];
|
||||
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
||||
const path = join(directory, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (skippedDirectories.has(entry.name)) continue;
|
||||
if (entry.name.endsWith('-e2e')) continue;
|
||||
files.push(...listProductionSources(path));
|
||||
} else if (
|
||||
entry.name.endsWith('.ts') &&
|
||||
!entry.name.endsWith('.d.ts') &&
|
||||
!testOnlyFile.test(entry.name)
|
||||
) {
|
||||
files.push(relative(workspaceRoot, path).split(sep).join('/'));
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function readSources(): Map<string, string> {
|
||||
const sources = new Map<string, string>();
|
||||
for (const root of sourceRoots) {
|
||||
for (const file of listProductionSources(join(workspaceRoot, root))) {
|
||||
sources.set(file, readFileSync(join(workspaceRoot, file), 'utf8'));
|
||||
}
|
||||
}
|
||||
return sources;
|
||||
}
|
||||
|
||||
function readEagerChecklist(): { open: string[]; done: string[] } {
|
||||
const markdown = readFileSync(join(workspaceRoot, checklistPath), 'utf8');
|
||||
const section = markdown.split(/^## Eager components$/m)[1];
|
||||
assert.ok(
|
||||
section,
|
||||
`${checklistPath} must have an "Eager components" section`
|
||||
);
|
||||
const body = section.split(/^## /m)[0];
|
||||
const open: string[] = [];
|
||||
const done: string[] = [];
|
||||
for (const match of body.matchAll(/^- \[( |x)\] `([^`]+\.ts)`/gm)) {
|
||||
(match[1] === 'x' ? done : open).push(match[2]);
|
||||
}
|
||||
return { open: open.sort(), done: done.sort() };
|
||||
}
|
||||
|
||||
const sources = readSources();
|
||||
|
||||
test('the zoneless checklist lists exactly the components that are still Eager', () => {
|
||||
const eager = [...sources]
|
||||
.filter(([, text]) => text.includes('ChangeDetectionStrategy.Eager'))
|
||||
.map(([file]) => file)
|
||||
.sort();
|
||||
const { open } = readEagerChecklist();
|
||||
|
||||
assert.deepEqual(
|
||||
eager,
|
||||
open,
|
||||
`Production files with ChangeDetectionStrategy.Eager must match the unticked entries in ${checklistPath}. ` +
|
||||
'Do not add Eager components; tick an entry when its component is converted.'
|
||||
);
|
||||
});
|
||||
|
||||
test('ticked checklist entries name files that exist', () => {
|
||||
for (const file of readEagerChecklist().done) {
|
||||
assert.ok(sources.has(file), `${file} is ticked but does not exist`);
|
||||
}
|
||||
});
|
||||
|
||||
test('no production component uses the deprecated Default strategy alias', () => {
|
||||
for (const [file, text] of sources) {
|
||||
assert.doesNotMatch(text, /ChangeDetectionStrategy\.Default\b/, file);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user