Angular Performance
Understand how to improve Angular application performance using lazy loading, optimized change detection, signals, efficient list rendering, deferred content, caching, bundle optimization, and careful template design.
Lesson content
What is Angular Performance? Angular performance means making an application load quickly, respond smoothly, render efficiently, and use browser resources carefully. Simple Formula Angular Performance = Fast Initial Load + Efficient Rendering + Responsive User Interface + Optimized Network Requests + Small Bundles Main Performance Areas Area Goal Bundle performance Download less JavaScript Rendering performance Update only required views Runtime performance Avoid expensive repeated work Network performance Reduce unnecessary API requests Memory performance Clean up resources and subscriptions Perceived performance Show useful content quickly 1. Lazy Loading Lazy loading downloads a feature only when the user navigates to it. import { Routes } from '@angular/router'; export const routes: Routes = [ { path: 'orders', loadChildren: () => import( './features/orders/order.routes' ).then( module => module.ORDER_ROUTES ) } ]; Lazy Component Loading { path: 'dashboard', loadComponent: () => import( './dashboard.component' ).then( module => module.DashboardComponent ) } Benefit Without Lazy Loading: Initial Bundle = Home + Users + Orders + Reports + Settings With Lazy Loading: Initial Bundle = Required Startup Code Other Features = Loaded When Needed 2. Route Preloading Preloading can download lazy routes in the background after the application starts. import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router'; provideRouter( routes, withPreloading( PreloadAllModules ) ); Use preloading when users are likely to visit lazy-loaded features soon after startup. 3. OnPush Change Detection OnPush allows Angular to skip unaffected component subtrees. import { ChangeDetectionStrategy, Component, input } from '@angular/core'; @Component({ selector: 'app-user-card', changeDetection: ChangeDetectionStrategy.OnPush, template: ` <h2> {{ user().name }} </h2> ` }) export class UserCardComponent { user = input.required<User>(); } Good OnPush Update this.user = { ...this.user, name: 'Rahul' }; Avoid Mutable Updates this.user.name = 'Rahul'; 4. Signals Signals allow Angular to track reactive values used by templates. import { computed, signal } from '@angular/core'; count = signal(0); doubleCount = computed( () => this.count() * 2 ); Template <p> {{ count() }} </p> <p> {{ doubleCount() }} </p> Signal Flow Signal Changes ↓ Dependent Computed Values Update ↓ Affected View Marked ↓ Required DOM Updated 5. Efficient List Rendering Always provide a stable tracking expression when rendering lists. @for ( user of users(); track user.id ) { <app-user-card [user]="user" /> } Avoid Unstable Tracking @for ( user of users(); track $index ) { ... } Tracking by index can cause unnecessary DOM reuse problems when items are inserted, deleted, or reordered. Best Tracking Key track user.id Use a unique and stable identifier. 6. Avoid Methods in Templates Avoid <p> {{ calculateTotal() }} </p> Template methods can execute repeatedly during view checks. Prefer a Computed Signal total = computed(() => this.items() .reduce( ( sum, item ) => sum + item.price, 0 ) ); <p> {{ total() }} </p> Alternative Calculate the value when the underlying data changes and store the result in a property. 7. Pure Pipes Pure pipes recalculate only when their input reference or primitive value changes. import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ name: 'fullName', pure: true }) export class FullNamePipe implements PipeTransform { transform( firstName: string, lastName: string ): string { return `${firstName} ${lastName}`; } } Usage {{ firstName | fullName:lastName }} Avoid Impure Pipes @Pipe({ name: 'filterUsers', pure: false }) An impure pipe may run frequently and can become expensive. 8. Deferred Loading with @defer The @defer block delays loading and rendering non-critical content. @defer { <app-heavy-chart /> } @placeholder { <p> Chart will load shortly </p> } @loading { <p> Loading chart... </p> } @error { <p> Unable to load chart </p> } Viewport Trigger @defer ( on viewport ) { <app-report-chart /> } Interaction Trigger @defer ( on interaction ) { <app-comments /> } Benefit Critical Content = Load Immediately Non-Critical Content = Load Later 9. Optimize RxJS Streams Search Example this.searchControl .valueChanges .pipe( debounceTime(300), distinctUntilChanged(), switchMap(search => this.productService .searchProducts(search) ) ); Why It Helps Every Key Press → debounceTime() Repeated Same Value → distinctUntilChanged() Old HTTP Request → switchMap Cancels It 10. Avoid Nested Subscriptions Avoid this.route.params .subscribe(params => { this.userService .getUser(params['id']) .subscribe(user => { this.user = user; }); }); Prefer this.route.params .pipe( switchMap(params => this.userService .getUser( params['id'] ) ) ); This improves cancellation, readability, and resource management. 11. Use AsyncPipe The AsyncPipe subscribes to an Observable, updates the view, and handles subscription cleanup. @if ( users$ | async; as users ) { @for ( user of users; track user.id ) { <p> {{ user.name }} </p> } } Benefits Automatic subscription management Works with reactive change detection Less component code Reduces memory-leak risk 12. Subscription Cleanup For manual subscriptions, use Angular's destruction utilities. import { DestroyRef, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; export class UserComponent { private destroyRef = inject(DestroyRef); constructor() { this.userService.users$ .pipe( takeUntilDestroyed( this.destroyRef ) ) .subscribe(users => { console.log(users); }); } } 13. Cache Shared HTTP Results When multiple consumers need the same request result, cache it carefully. import { shareReplay } from 'rxjs'; users$ = this.http .get<User[]>( '/api/users' ) .pipe( shareReplay({ bufferSize: 1, refCount: true }) ); Without Sharing Subscriber 1 → HTTP Request Subscriber 2 → Another HTTP Request With Shared Result One HTTP Request ↓ Shared Result ↓ Multiple Subscribers Cache only data with a clear refresh or invalidation strategy. 14. Avoid Duplicate API Requests Store commonly reused results in a service or state store rather than requesting the same data repeatedly. Request Data ↓ Store Result ↓ Reuse Result ↓ Refresh When Necessary 15. Pagination and Virtual Scrolling Do not render thousands of items at once. Pagination GET /api/users?page=1&limit=20 Virtual Scrolling Virtual scrolling renders only the items currently visible in the viewport. <cdk-virtual-scroll-viewport itemSize="50" > <div *cdkVirtualFor=" let user of users " > {{ user.name }} </div> </cdk-virtual-scroll-viewport> Benefit 10,000 Records ≠ 10,000 DOM Elements Virtual Scroll = Render Visible Items Only 16. Optimize Images Large images can significantly delay page rendering. Recommended Actions Use appropriately sized images Compress image files Use modern image formats Lazy-load below-the-fold images Provide width and height Prioritize important hero images Angular Image Directive <img ngSrc="/images/product.webp" width="400" height="300" alt="Product" /> 17. Bundle Optimization Production Build ng build A production build applies optimizations such as minification, dead-code removal, and bundling. Inspect Bundle Size ng build --stats-json Use a bundle-analysis tool to identify large dependencies. Avoid Large Imports Avoid importing an entire utility library for one small function. Prefer smaller, tree-shakable imports where supported. 18. Tree Shaking Tree shaking removes unused code from the production bundle. Tree-Shakable Service @Injectable({ providedIn: 'root' }) export class LoggerService {} Concept Application Code + Dependencies ↓ Build Analysis ↓ Unused Code Removed ↓ Smaller Bundle 19. Avoid Heavy Third-Party Libraries Before installing a dependency, check: Bundle size Tree-shaking support Maintenance status Browser support Whether Angular already provides the feature 20. Web Workers Web workers move CPU-intensive calculations away from the browser's main thread. Generate a Worker ng generate web-worker report Use Cases Large calculations Data transformation File processing Complex report generation Flow Main Thread ↓ Send Data to Worker ↓ Worker Calculates ↓ Returns Result ↓ UI Remains Responsive 21. Run Frequent Work Outside Angular In zone-based applications, frequent events can trigger unnecessary change-detection activity. this.zone.runOutsideAngular( () => { window.addEventListener( 'scroll', this.handleScroll ); } ); Re-enter Angular only when displayed state must change. this.zone.run(() => { this.showButton = true; }); 22. Server-Side Rendering Server-side rendering can send initial HTML before the browser finishes loading the application JavaScript. Browser Request ↓ Server Renders HTML ↓ HTML Displayed ↓ Angular Hydrates Page ↓ Application Becomes Interactive Benefits Faster initial content display Improved search-engine visibility Better perceived loading performance 23. Avoid Repeated Expensive Computation Avoid get filteredUsers() { return this.users.filter( user => user.active ); } A getter used in the template may execute repeatedly. Prefer activeUsers = computed(() => this.users() .filter( user => user.active ) ); 24. Split Large Components Large components can become difficult to render, test, and maintain. Large Dashboard Component ↓ Split Into ↓ Summary Component Chart Component Table Component Filter Component Smaller components create clearer update boundaries. 25. Avoid Unnecessary DOM Elements Avoid <div> <div> <div> <p> User </p> </div> </div> </div> Prefer <p> User </p> A smaller DOM is generally faster to create, style, and update. 26. Use Development Profiling Tools Use browser and Angular profiling tools to find actual bottlenecks. Measure Component render frequency Long-running JavaScript tasks Network request duration Bundle size Memory growth Layout and paint activity Performance Rule Measure ↓ Identify Bottleneck ↓ Optimize ↓ Measure Again Performance Optimization Example Before @Component({ template: ` @for ( product of getProducts(); track $index ) { <p> {{ calculatePrice(product) }} </p> } ` }) export class ProductListComponent { getProducts() { return this.products .filter( product => product.active ); } calculatePrice( product: Product ) { return product.price * product.quantity; } } After @Component({ changeDetection: ChangeDetectionStrategy.OnPush, template: ` @for ( product of activeProducts(); track product.id ) { <p> {{ product.total }} </p> } ` }) export class ProductListComponent { products = signal<Product[]>([]); activeProducts = computed(() => this.products() .filter( product => product.active ) .map( product => ({ ...product, total: product.price * product.quantity }) ) ); } Common Performance Mistakes 1. Loading Every Feature at Startup Use route lazy loading and deferred views. 2. Calling Methods in Templates Use properties, computed signals, or pure pipes. 3. Rendering Large Lists Without Tracking Use a stable identifier such as track item.id . 4. Rendering Thousands of DOM Elements Use pagination or virtual scrolling. 5. Making Duplicate HTTP Requests Share, cache, or store results where appropriate. 6. Keeping Unused Subscriptions Active Use AsyncPipe or automatic destruction utilities. 7. Using Large Libraries for Small Tasks Prefer smaller dependencies or native browser APIs. 8. Mutating Input Objects Use immutable object and array updates. 9. Using Expensive Impure Pipes Prefer pure pipes or computed state. 10. Optimizing Without Measurement Use profiling tools to find real bottlenecks before adding complexity. Best Practices Lazy-load large features Use deferred loading for non-critical content Use OnPush-compatible reactive patterns Use signals and computed values for UI state Track list items with stable identifiers Avoid methods and expensive getters in templates Use AsyncPipe for Observable values Clean up manual subscriptions Debounce frequent user input Cancel outdated requests with switchMap Use pagination or virtual scrolling for large lists Optimize images and third-party dependencies Use web workers for CPU-intensive tasks Measure performance before and after optimization Interview Questions How can Angular application performance be improved? Use lazy loading, OnPush-compatible updates, signals, efficient list tracking, deferred loading, optimized RxJS streams, smaller bundles, and careful template design. Why should template methods be avoided? They can execute repeatedly during view checks and may perform unnecessary work. Why is list tracking important? It helps Angular reuse existing DOM elements instead of recreating the entire list. What is lazy loading? Lazy loading downloads a route or feature only when it is required. What is @defer? It delays loading and rendering non-critical template content until a configured trigger occurs. How does switchMap improve performance? It unsubscribes from an older inner stream when a newer value arrives, which is useful for cancelling outdated search requests. Why use AsyncPipe? It manages subscriptions automatically and notifies Angular when new values arrive. What is virtual scrolling? Virtual scrolling renders only the items visible in the viewport rather than rendering an entire large collection. What is tree shaking? Tree shaking removes unused code from a production bundle. When should a web worker be used? Use a web worker for CPU-intensive work that could block the browser's main user-interface thread. Key Point Angular performance improves when the application downloads less code, performs less repeated work, renders fewer elements, and updates only the views affected by state changes. Easy Formula Fast Angular Application = Lazy Loading + OnPush Patterns + Signals + Stable List Tracking + Efficient RxJS + Small Bundles + Optimized Images Performance Process = Measure → Find Bottleneck → Optimize → Measure Again