| technology | Angular | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| domain | frontend | ||||||||||||||||||||
| level | Senior/Architect | ||||||||||||||||||||
| version | 20 | ||||||||||||||||||||
| tags |
|
||||||||||||||||||||
| ai_role | Senior Angular Performance Expert | ||||||||||||||||||||
| last_updated | 2026-03-22 |
- Primary Goal: Enforce strict adherence to modern Angular v20 patterns, specifically Zoneless reactivity and functional APIs for optimal best practices.
- Target Tooling: Cursor, Windsurf, Antigravity.
- Tech Stack Version: Angular 20
Important
Strict Constraints for AI:
- Always use
signal(),computed(), andeffect()instead of RxJSBehaviorSubjectfor local state. - Never use
@Input()or@Output()decorators; strictly useinput()andoutput()functional APIs. - Always utilize the built-in control flow (
@if,@for,@switch) instead of structural directives (*ngIf,*ngFor).
Note
Context: Component Inputs
@Input() title: string = '';The @Input() decorator operates outside the Signals reactivity system. Changes are not tracked granularly, requiring checks of the entire component tree (Dirty Checking) via Zone.js.
title = input<string>('');Use Signal Inputs (input()). This allows Angular to precisely know which specific component requires an update, paving the way for Zoneless applications.
Note
Context: Component Outputs
@Output() save = new EventEmitter<void>();The classic EventEmitter adds an unnecessary layer of abstraction over RxJS Subject and does not integrate with the Angular functional API.
save = output<void>();Use the output() function. It provides strict typing, better performance, and a unified API with Signal Inputs.
Note
Context: Model Synchronization
@Input() value: string;
@Output() valueChange = new EventEmitter<string>();Boilerplate code that is easy to break if you make a mistake in naming the Change event.
value = model<string>();Use model(). This creates a Signal that can be both read and written to, automatically synchronizing its state with the parent.
Note
Context: Template Control Flow
<div *ngIf="isLoaded; else loading">
<li *ngFor="let item of items">{{ item }}</li>
</div>Directives require importing CommonModule or NgIf/NgFor, increasing bundle size. Micro-template syntax is complex for static analysis and type-checking.
@if (isLoaded()) {
@for (item of items(); track item.id) {
<li>{{ item.name }}</li>
}
} @else {
<app-loader />
}Use the built-in Control Flow (@if, @for). It is built into the compiler, requires no imports, supports improved type-narrowing, and runs faster.
Note
Context: Data Fetching
data: unknown;
ngOnInit() {
this.service.getData().subscribe(res => this.data = res);
}Imperative subscriptions lead to memory leaks (if you forget to unsubscribe), "Callback Hell", and state desynchronization. Requires manual subscription management.
data = toSignal(this.service.getData());Use toSignal() to convert an Observable into a Signal. This automatically manages the subscription and integrates the data stream into the reactivity system.
Note
Context: Component State Management
private count$ = new BehaviorSubject(0);
getCount() { return this.count$.value; }RxJS is overkill for simple synchronous state. BehaviorSubject requires .value for access and .next() for writes, increasing cognitive load.
count = signal(0);
// Access: count()
// Update: count.set(1)Use signal() for local state. It is a primitive designed specifically for synchronizing UI and data.
Note
Context: Reactivity
ngOnChanges(changes: SimpleChanges) {
if (changes['firstName']) {
this.fullName = `${this.firstName} ${this.lastName}`;
}
}ngOnChanges is triggered only when Inputs change, has complex typing, and runs before View initialization.
fullName = computed(() => `${this.firstName()} ${this.lastName()}`);Use computed(). The signal is recalculated only when its dependencies change, and the result is memoized (cached).
Note
Context: DI Pattern
constructor(private http: HttpClient, private store: Store) {}Constructors become cluttered with many dependencies. When inheriting classes, dependencies must be passed through super().
private http = inject(HttpClient);
private store = inject(Store);Use the inject() function. It operates in the initialization context (fields or constructor), is type-safe, and does not require super() during inheritance.
Note
Context: App Architecture
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule]
})
export class AppModule {}Modules create an unnecessary level of indirection. Components become dependent on the module context, complicating Lazy Loading and testing.
@Component({
standalone: true,
imports: [CommonModule]
})Use Standalone Components. This is the Angular v14+ standard that makes components self-sufficient and tree-shakable.
Note
Context: Lazy Loading Routing
loadChildren: () => import('./module').then(m => m.UserModule)Loading modules pulls in transitive dependencies that might not be needed.
loadComponent: () => import('./user.component').then(c => c.UserComponent)Note
Context: Template Performance
<div>{{ calculateTotal(items) }}</div>The calculateTotal function is called during every Change Detection (CD) cycle, even if items have not changed. This kills UI performance.
total = computed(() => this.calculateTotal(this.items()));<div>{{ total() }}</div>Extract logic into computed() signals or Pure Pipes. They are only executed when input data changes.
Note
Context: RxJS Memory Leaks
destroy$ = new Subject<void>();
ngOnDestroy() { this.destroy$.next(); }
stream$.pipe(takeUntil(this.destroy$)).subscribe();It's easy to forget takeUntil or unsubscribe. Requires a lot of boilerplate code in every component.
stream$.pipe(takeUntilDestroyed()).subscribe();Use the takeUntilDestroyed() operator. It automatically unsubscribes upon context destruction (component, directive, service).
Note
Context: Prop Drilling
<!-- Parent -->
<app-child [theme]="theme"></app-child>
<!-- Child -->
<app-grandchild [theme]="theme"></app-grandchild>"Prop drilling" heavily couples intermediate components to data they don't need, just for the sake of passing it deeper.
// Service
theme = signal('dark');
// Grandchild
theme = inject(ThemeService).theme;Use Signal Stores or services for state sharing, or the new input() API with context inheritance (in the future).
Note
Context: Security & Abstraction
el.nativeElement.style.backgroundColor = 'red';Direct DOM access breaks abstraction (doesn't work in SSR/Web Workers) and opens up XSS vulnerabilities. It bypasses Angular Sanitization mechanisms.
// Use Renderer2 or bindings
<div [style.background-color]="color()"></div>Note
Context: Change Detection
The application relies on Zone.js for any asynchronous event (setTimeout, Promise, XHR).
Zone.js patches all browser APIs, adding overhead and increasing bundle size. CD triggers more often than necessary.
bootstrapApplication(App, {
providers: [provideExperimentalZonelessChangeDetection()]
});For further reading, please refer to the following specialized guides: