Performance optimization in Angular requires understanding bundle sizes, runtime performance, rendering efficiency, and network optimization. The key insight is that performance is not one problem — it is four distinct problems that happen at different times in your app’s lifecycle, and the fix for one can actually worsen another if you are not careful.The practical framework: Before optimizing anything, measure first. Use Chrome DevTools Performance tab, Lighthouse, and ng build --stats-json with webpack-bundle-analyzer. Premature optimization based on intuition rather than profiling is the number one waste of engineering time in Angular projects. The 80/20 rule applies heavily: OnPush change detection + lazy loading + proper track in @for loops will solve 80% of performance problems in 20% of the effort.
Deferrable views are Angular’s answer to the question “how do I lazy-load something that is not a route?” Before @defer, you had to use dynamic imports with NgComponentOutlet or build custom intersection observer logic. Now the framework handles all of that with declarative syntax. The code for the deferred component is not even included in the initial bundle — it is split into a separate chunk automatically.
Practical tip: Use @defer (on viewport; prefetch on idle) for below-the-fold content. This prefetches the chunk during browser idle time, so by the time the user scrolls down, the code is already cached and the component renders instantly. This gives you the bundle size benefit of lazy loading with zero perceived latency.
@Component({ template: ` <!-- Defer loading until user scrolls to viewport --> @defer (on viewport) { <app-heavy-chart [data]="chartData()" /> } @placeholder { <div class="chart-skeleton"></div> } @loading (minimum 500ms) { <app-spinner /> } <!-- Defer until interaction --> @defer (on interaction) { <app-comments [postId]="postId()" /> } @placeholder { <button>Load Comments</button> } <!-- Defer on idle --> @defer (on idle) { <app-recommendations /> } <!-- Defer with timer --> @defer (on timer(2s)) { <app-ads /> } <!-- Prefetch strategies --> @defer (on viewport; prefetch on idle) { <app-product-gallery [images]="images()" /> } @placeholder { <div class="gallery-placeholder"></div> } `})export class ProductPageComponent { chartData = input<ChartData>(); postId = input<string>(); images = input<string[]>();}
Memory leaks from unsubscribed observables are the silent killer of Angular app performance. Every subscription that outlives its component is a function reference that prevents garbage collection — and each one adds overhead to every change detection cycle. In production, this manifests as the app gradually slowing down the longer a user keeps a tab open.
@Component({ template: `...`})export class DataComponent { private destroyRef = inject(DestroyRef); private dataService = inject(DataService); data = signal<Data[]>([]); ngOnInit() { // takeUntilDestroyed() automatically completes the observable // when the component's DestroyRef fires. This is the modern // replacement for the manual Subject + takeUntil + ngOnDestroy pattern. this.dataService.getData() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(data => this.data.set(data)); // Or use effect for automatic cleanup effect(() => { const subscription = this.dataService.stream$.subscribe( item => this.processItem(item) ); return () => subscription.unsubscribe(); }); } private processItem(item: DataItem) { this.data.update(current => [...current, item]); }}// Using AsyncPipe (auto-cleanup)@Component({ template: ` @if (data$ | async; as data) { @for (item of data; track item.id) { <app-item [item]="item" /> } } `})export class DataListComponent { data$ = inject(DataService).getData();}
Client-side HTTP caching can eliminate redundant network requests entirely. The simplest pattern is a Map-based cache for GET requests. For more sophisticated needs, the “stale-while-revalidate” pattern (shown below) gives users instant responses from cache while silently refreshing data in the background — this is the same strategy that Chrome’s HTTP cache uses and what makes apps feel “snappy.”
Pitfall: A naive cache interceptor with no TTL or invalidation will serve stale data forever. Always pair caching with either a max-age, a manual invalidation mechanism, or the stale-while-revalidate pattern. For data that changes frequently (e.g., stock prices, notifications), do not cache at the HTTP level — use the service worker’s freshness strategy instead.