| 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 advanced performance best practices.
- Target Tooling: Cursor, Windsurf, Antigravity.
- Tech Stack Version: Angular 20
Note
Context: Bundle Size
<app-chart [data]="data" />A charting library (e.g., ECharts) loads immediately, blocking TTI (Time to Interactive), even if the chart is below the "fold".
@defer (on viewport) {
<app-chart [data]="data" />
} @placeholder {
<div>Loading chart...</div>
}Use @defer. This defers component code loading until a trigger occurs (viewport, interaction, timer).
Note
Context: Event Loop Blocking
Sorting an array of 100k elements directly in the component.
Freezes the UI.
Offload computations to a Web Worker.
Use Angular Web Workers. In v20, this is easily configured via the CLI.
Note
Context: Signal Effects
effect(() => {
const timer = setInterval(() => ..., 1000);
// No cleanup
});Effects restart when dependencies change. If you don't clean up timers/subscriptions inside an effect, they accumulate.
effect((onCleanup) => {
const timer = setInterval(() => ..., 1000);
onCleanup(() => clearInterval(timer));
});Always use the onCleanup callback to release resources.
Note
Context: Zone Integration
Wrapping third-party libraries in ngZone.run() unnecessarily.
Forces redundant checks of the entire component tree.
ngZone.runOutsideAngular(() => {
// Heavy chart rendering or canvas animation
});Run frequent events (scroll, mousemove, animationFrame) outside the Angular zone. Update signals only when UI updates are required.
Note
Context: Signal Performance
data = signal({ id: 1 }, { equal: undefined }); // Default checks referenceIf you create a new object with the same data { id: 1 }, the signal triggers an update, even though the data hasn't fundamentally changed.
import { isEqual } from 'lodash-es';
data = signal(obj, { equal: isEqual });Use a custom comparison function for complex objects to avoid redundant re-renders.
Note
Context: Re-rendering Lists
<li *ngFor="let item of items">{{ item }}</li>Without tracking, any array change leads to the recreation of all DOM nodes in the list.
@for (item of items; track item.id)Always use a unique key in track. This allows Angular to move DOM nodes instead of recreating them.
Note
Context: Tree Rendering
<ng-template #tree let-node>
{{ node.name }}
<ng-container *ngFor="let child of node.children">
<ng-container *ngTemplateOutlet="tree; context: { $implicit: child }"></ng-container>
</ng-container>
</ng-template>
<ng-container *ngTemplateOutlet="tree; context: { $implicit: root }"></ng-container>Recursive component calls without OnPush or memoization cause exponential growth in change detection checks, blocking the main thread during deep tree rendering.
@Component({
selector: 'app-tree-node',
standalone: true,
imports: [CommonModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
{{ node().name }}
@for (child of node().children; track child.id) {
<app-tree-node [node]="child" />
}
`
})
export class TreeNodeComponent {
node = input.required<TreeNode>();
}Use standalone components with ChangeDetectionStrategy.OnPush and modern @for control flow for recursive structures. This ensures change detection only runs when inputs change, drastically improving performance for deeply nested trees.
Note
Context: CSS Encapsulation
/* global.css */
button { padding: 10px; }Global styles unpredictably affect components.
Use ViewEncapsulation.Emulated (default) and specific selectors.
Keep styles locally within components.
Note
Context: Split Chunks
A single huge component of 3000 lines.
Poor readability, rendering lazy loading of UI parts impossible.
Decompose into "dumb" (UI) and "smart" components.
Break down the UI into small, reusable blocks.
Note
Context: Core Web Vitals (LCP)
<img src="large-hero.jpg" />The browser loads the full image, shifting the layout (CLS).
<img ngSrc="hero.jpg" width="800" height="600" priority />Use the NgOptimizedImage directive. It automatically handles lazy loading, preconnect, and srcset.
Note
Context: SSR / SSG
Rendering Date.now() or random numbers (Math.random()) directly in the template.
The server generates one number, the client another. This causes "flickering" and a hydration error; Angular discards the server DOM and renders from scratch.
Use stable data or defer random generation until afterNextRender.
Pay attention to template determinism with SSR.
Note
Context: DI Performance
processItems(items: Item[]) {
items.forEach(item => {
const logger = inject(LoggerService);
logger.log(item.name);
});
}Although inject() is fast, calling it inside hot paths (loops) triggers unnecessary Dependency Injection tree lookups on every iteration, which degrades performance.
export class ItemProcessor {
private logger = inject(LoggerService);
processItems(items: Item[]) {
items.forEach(item => {
this.logger.log(item.name);
});
}
}Inject dependencies exactly once at the class or property level. This caches the reference to the service, bypassing redundant DI resolution and keeping hot paths efficient.
Note
Context: Signal Graph
effect(() => {
console.log('Value changed:', this.value());
this.analytics.track('Change', this.user()?.id);
});Angular dynamically builds the signal graph. If you read a signal like this.user() inside an effect just for analytics, any change to user() will unexpectedly re-trigger the effect, leading to redundant executions.
effect(() => {
const currentVal = this.value();
untracked(() => {
this.analytics.track('Change', this.user()?.id);
});
console.log('Value changed:', currentVal);
});Important
Use untracked() to read signals that MUST not register as dependencies. This prevents unintended re-evaluations and ensures effects only run when their primary state changes.
Note
Context: DOM Size
<div *ngIf="isLoggedIn()">
<div class="user-panel">
<app-user-profile></app-user-profile>
</div>
</div>Unnecessary wrapper <div> elements deeply nest the DOM tree ("div soup"). This exponentially slows down CSS Style Recalculation, Layout (Reflow), and Paint.
@if (isLoggedIn()) {
<ng-container>
<app-user-profile class="user-panel"></app-user-profile>
</ng-container>
}Utilize <ng-container> to apply structural logic or apply classes directly to component hosts. <ng-container> is rendered as an invisible comment, keeping the DOM tree shallow and performant.
Note
Context: High-frequency events
@HostListener('window:scroll', ['$event'])
onScroll() {
this.scrollPosition.set(window.scrollY);
}Every scroll, mousemove, or drag event triggers a full Angular Change Detection cycle. High-frequency events will cause immediate UI lag and frame drops.
export class ScrollTracker {
private zone = inject(NgZone);
scrollPosition = signal(0);
constructor() {
this.zone.runOutsideAngular(() => {
window.addEventListener('scroll', () => {
if (Math.abs(window.scrollY - this.scrollPosition()) > 50) {
this.zone.run(() => this.scrollPosition.set(window.scrollY));
}
});
});
}
}