How Do You Improve Angular Application Performance?
What is Angular Performance Optimization?
Angular performance optimization means reducing unnecessary work, loading less JavaScript/data, and rendering the UI efficiently so that the application:
- Loads faster
- Responds faster to user actions
- Performs less unnecessary change detection
- Uses less memory
- Makes fewer unnecessary API calls
- Handles large lists efficiently
The major areas are:
Angular Performance
│
├── Change Detection
├── Signals
├── Lazy Loading
├── @defer
├── Efficient Rendering
├── RxJS / Subscription Management
├── HTTP / API Optimization
├── Bundle Optimization
└── SSR / Hydration when appropriate
Full Simple Sample Program
This example combines several useful techniques:
OnPush- Signals
computed()@forwithtrack- Lazy-loaded routes
app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./home').then(m => m.Home)
},
{
path: 'products',
loadComponent: () =>
import('./products').then(m => m.Products)
}
];
products.ts
import {
ChangeDetectionStrategy,
Component,
computed,
signal
} from '@angular/core';
interface Product {
id: number;
name: string;
price: number;
}
@Component({
selector: 'app-products',
standalone: true,
templateUrl: './products.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class Products {
products = signal<Product[]>([
{ id: 1, name: 'Laptop', price: 50000 },
{ id: 2, name: 'Mouse', price: 1000 },
{ id: 3, name: 'Keyboard', price: 2000 }
]);
search = signal('');
filteredProducts = computed(() => {
const value = this.search().toLowerCase();
return this.products().filter(product =>
product.name.toLowerCase().includes(value)
);
});
updateSearch(event: Event): void {
const input = event.target as HTMLInputElement;
this.search.set(input.value);
}
}
products.html
<h2>Products</h2>
<input
type="text"
placeholder="Search product"
(input)="updateSearch($event)"
/>
@for (product of filteredProducts(); track product.id) {
<div>
<h3>{{ product.name }}</h3>
<p>
Price: {{ product.price }}
</p>
</div>
}
Program Explanation
1. Lazy Loading
Instead of importing Products into the initial application bundle:
import { Products } from './products';
we use:
loadComponent: () =>
import('./products').then(m => m.Products)
This allows Angular to load that component's code when the route is needed.
Application starts
↓
Initial code loaded
↓
User opens /products
↓
Products chunk loaded
This reduces the initial JavaScript payload.
2. Use OnPush
We configured:
changeDetection: ChangeDetectionStrategy.OnPush
OnPush allows Angular to skip component subtrees when they don't need checking.
Default
↓
Normal eager checking
OnPush
↓
Skip unaffected subtrees
This can be valuable in large component trees.
3. Use Signals
Instead of:
products: Product[] = [];
search = '';
we use:
products = signal<Product[]>([]);
search = signal('');
Signals provide Angular with explicit reactive dependencies.
Signal changes
↓
Angular knows its consumers
↓
Relevant views can be refreshed
Signals work particularly well with OnPush and zoneless-compatible Angular applications.
4. Use computed() for Derived Data
Instead of repeatedly putting filtering logic into the template, we create:
filteredProducts = computed(() => {
const value = this.search().toLowerCase();
return this.products().filter(product =>
product.name.toLowerCase().includes(value)
);
});
computed() tracks:
products
+
search
↓
filteredProducts
It recalculates when its dependencies change and otherwise retains its computed value until invalidated.
This is useful for derived state.
5. Use track with @for
Consider:
@for (product of filteredProducts(); track product.id) {
The important part is:
track product.id
Angular can identify each item using its unique ID.
Suppose:
Product 1
Product 2
Product 3
becomes:
Product 1
Product 2
Product 3
Product 4
Tracking by ID helps Angular efficiently reconcile the collection rather than treating existing items as unrelated.
For modern Angular:
@for (product of products(); track product.id) {
For older *ngFor, you commonly used:
<div *ngFor="let product of products; trackBy: trackById">
Important Angular Performance Techniques
1. Use Lazy Loading
One of the most important optimizations.
Instead of loading the entire application immediately:
Login
Dashboard
Products
Orders
Reports
Admin
Settings
↓
Everything loaded initially
lazy load features:
Initial Load
↓
Only required code
User opens Reports
↓
Reports code loaded
Modern standalone Angular:
{
path: 'products',
loadComponent: () =>
import('./products').then(m => m.Products)
}
For a group of routes:
{
path: 'admin',
loadChildren: () =>
import('./admin/admin.routes')
.then(m => m.ADMIN_ROUTES)
}
2. Use OnPush Where Appropriate
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
This lets Angular skip unnecessary component subtree checks.
It works particularly well with:
OnPush
+
Signals
+
Immutable data
+
AsyncPipe
3. Use Signals for Reactive State
Instead of relying on arbitrary mutable state:
count = 0;
consider:
count = signal(0);
Update:
this.count.update(value => value + 1);
Angular can track Signal consumers and refresh affected views.
4. Consider Zoneless Angular
Modern Angular supports running without Zone.js.
Instead of broadly relying on patched asynchronous APIs:
Async operation
↓
Zone.js
↓
Angular synchronization
zoneless Angular uses Angular-recognized notifications such as:
Signals
Template events
AsyncPipe
markForCheck()
Input changes
This can make change notification more explicit and reduce Zone.js-related overhead.
However, don't migrate an application to zoneless only because "zoneless must be faster." Verify application/library compatibility and measure the actual benefit.
5. Use @for with track
Avoid unstable identity.
Prefer:
@for (product of products(); track product.id) {
<p>
{{ product.name }}
</p>
}
A stable unique key such as:
product.id
helps Angular efficiently update collections.
6. Use @defer
Modern Angular supports deferred loading of parts of a template.
For example:
<h1>Dashboard</h1>
@defer (on viewport) {
<app-sales-chart />
} @placeholder {
<p>Chart will load when needed...</p>
}
Conceptually:
Page loads
↓
Important content displayed
↓
User approaches chart
↓
Chart dependencies loaded
This is particularly useful for:
- Charts
- Heavy components
- Content below the fold
- Optional widgets
- Large third-party libraries
7. Avoid Expensive Template Computations
Be careful with code such as:
<p>{{ calculateTotal() }}</p>
If calculateTotal() performs expensive work, repeated template evaluation can become costly.
For derived reactive state, prefer something like:
total = computed(() =>
this.products()
.reduce((sum, p) => sum + p.price, 0)
);
Template:
<p>{{ total() }}</p>
Now the calculation is tied to its reactive dependencies.
8. Use AsyncPipe
Instead of manually subscribing everywhere:
this.productService.getProducts()
.subscribe(products => {
this.products = products;
});
when appropriate, expose the Observable:
products$ = this.productService.getProducts();
Template:
@for (product of products$ | async; track product.id) {
{{ product.name }}
}
AsyncPipe manages subscription/unsubscription and integrates with Angular's change detection.
9. Prevent Subscription Memory Leaks
If you manually subscribe:
this.service.getData()
.subscribe(...);
some long-lived Observables require cleanup.
Modern Angular provides:
takeUntilDestroyed()
Example:
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
constructor() {
this.service.notifications$
.pipe(takeUntilDestroyed())
.subscribe(value => {
console.log(value);
});
}
This helps prevent subscriptions from surviving longer than their component.
10. Optimize API Calls
Don't repeatedly call APIs unnecessarily.
Bad pattern:
Component loads
↓
GET products
Navigate away/back
↓
GET products
Another component
↓
GET same products
Depending on the data's freshness requirements, consider:
- Client-side caching
- Shared services
- RxJS caching patterns
- Server/API caching
- HTTP caching
Also avoid downloading unnecessary data.
Instead of returning:
10,000 products
use server-side:
Pagination
Filtering
Sorting
Search
For example:
GET /api/products?page=1&pageSize=20
11. Debounce Search API Calls
Suppose a user types:
L
La
Lap
Lapt
Lapto
Laptop
Without debouncing, you might make six API requests.
Use RxJS operators such as:
debounceTime(300)
distinctUntilChanged()
switchMap(...)
Conceptually:
User typing rapidly
↓
Wait 300 ms
↓
Latest stable search value
↓
API request
switchMap() is especially useful because it can unsubscribe from a previous in-flight Observable when a newer search arrives.
12. Use Pagination or Virtual Scrolling
Avoid rendering thousands of DOM elements simultaneously.
Instead of:
10,000 records
↓
10,000 DOM rows
use:
Pagination
or, for suitable UI scenarios:
Virtual Scrolling
Virtual scrolling renders approximately what the user currently needs to see rather than every item at once.
13. Optimize Images
Images are frequently a major source of page weight.
Use Angular's image optimization facilities such as:
<img
ngSrc="/images/product.jpg"
width="400"
height="300"
alt="Product"
/>
with:
NgOptimizedImage
Also:
- Use appropriately sized images
- Compress images
- Prefer modern formats when appropriate
- Avoid downloading huge images only to display tiny thumbnails
14. Production Builds and Bundle Size
Production builds perform optimizations such as:
- Minification
- Tree shaking
- Dead-code elimination/build optimizations
- Code splitting where applicable
Build using the production configuration:
ng build --configuration production
Also inspect bundle sizes rather than assuming they are acceptable.
Large dependencies can significantly increase initial load time.
15. Avoid Unnecessary Third-Party Libraries
Suppose you need one simple utility but import a very large package.
Tiny feature
↓
Large dependency
↓
Larger JavaScript bundle
Before adding a package, check:
- Bundle impact
- Tree-shaking support
- Whether Angular/browser APIs already solve the problem
- Whether a smaller package is available
16. SSR and Hydration
For public-facing applications, Angular SSR can improve how quickly useful HTML becomes available and can benefit SEO.
Browser-only rendering
Browser
↓
Download JavaScript
↓
Execute Angular
↓
Render content
With SSR:
Server
↓
Initial HTML
↓
Browser displays content
↓
Angular hydrates
↓
Application becomes interactive
SSR is particularly relevant to:
- Public websites
- Article sites
- E-commerce
- SEO-sensitive pages
It is not automatically required for every internal dashboard or admin application.
Most Important Performance Techniques
For interview preparation, remember these 10:
1. Lazy load routes/features
2. Use OnPush appropriately
3. Use Signals/computed for reactive state
4. Track @for items using stable IDs
5. Use @defer for heavy/non-critical UI
6. Avoid expensive template computations
7. Manage RxJS subscriptions correctly
8. Reduce/debounce/cache API requests
9. Paginate or virtualize large lists
10. Optimize bundle size and images
Key Points
- Measure first using browser/Angular profiling tools; don't optimize blindly.
- Reduce the initial bundle size with lazy loading and deferred loading.
- Reduce unnecessary view checking using
OnPushand appropriate reactive state. - Use Signals and
computed()for predictable reactive state and derived values. - Always use stable tracking for dynamic lists.
- Don't render huge datasets at once.
- Don't make unnecessary API requests.
- Prevent long-lived RxJS subscription leaks.
- Optimize images and third-party dependencies.
- SSR/hydration can improve public-facing application startup characteristics, but should be chosen based on application requirements.
Interview Questions and Answers
1. How do you improve Angular application performance?
Use lazy loading, OnPush, Signals, stable @for tracking, @defer, efficient RxJS patterns, API optimization, pagination/virtual scrolling, image optimization, and bundle-size optimization.
2. How does OnPush improve performance?
OnPush allows Angular to skip component subtrees that don't require checking, reducing unnecessary change-detection work.
changeDetection: ChangeDetectionStrategy.OnPush
3. How do Signals help performance?
Signals provide reactive dependency tracking. Angular can know which views consume changed Signals and schedule the appropriate refresh work.
count = signal(0);
4. How does lazy loading improve performance?
Lazy loading keeps feature code out of the initial bundle and loads it when needed.
loadComponent: () =>
import('./products').then(m => m.Products)
This can improve initial startup performance.
5. Why should we use track with @for?
It gives Angular stable identity for list items so Angular can efficiently reconcile additions, removals, moves, and updates.
@for (product of products(); track product.id) {
{{ product.name }}
}
6. What is @defer?
@defer allows Angular to defer loading/rendering a section and its dependencies until a specified trigger occurs.
@defer (on viewport) {
<app-chart />
}
It is useful for heavy or non-critical UI.
7. How do you optimize Angular search functionality?
Use RxJS operators such as:
debounceTime()
distinctUntilChanged()
switchMap()
This avoids making an API request for every keystroke and prevents stale search requests from dominating newer ones.
8. How do you handle thousands of records?
Prefer server-side pagination/filtering and, where appropriate, virtual scrolling rather than rendering thousands of DOM elements simultaneously.
9. Should we use zoneless Angular for performance?
Zoneless Angular can reduce reliance on broad Zone.js notifications and works well with explicit mechanisms such as Signals. However, migration should be based on compatibility and measured performance rather than assumption.
10. What is the most important rule for Angular performance optimization?
Measure the actual bottleneck first, then reduce unnecessary loading, rendering, change detection, computation, and network activity.
Interview Quick Answer
I improve Angular performance by reducing initial bundle size through lazy loading and
@defer, reducing unnecessary change detection usingOnPushand Signals, efficiently rendering lists with stabletrackkeys, optimizing RxJS subscriptions and API calls, paginating large datasets, optimizing images, and monitoring production bundle/runtime performance.