Performance Optimization Overview
Estimated Time: 3 hours | Difficulty: Advanced | Prerequisites: Change Detection, RxJS
Copy
┌─────────────────────────────────────────────────────────────────────────┐
│ Performance Optimization Layers │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Build-Time │ │
│ │ • Tree shaking • Code splitting │ │
│ │ • Minification • Compression │ │
│ │ • AOT compilation • Dead code elimination │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Load-Time │ │
│ │ • Lazy loading • Preloading strategies │ │
│ │ • Route-based • Component-level │ │
│ │ • Deferrable views • Initial bundle size │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Runtime │ │
│ │ • Change detection • OnPush strategy │ │
│ │ • Signals • Memory management │ │
│ │ • Virtual scrolling • TrackBy functions │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Rendering │ │
│ │ • DOM operations • CSS containment │ │
│ │ • Layout thrashing • Paint optimization │ │
│ │ • Animation perf • Web Workers │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
Bundle Size Optimization
Build Analysis
Copy
# Generate bundle analysis
ng build --stats-json
npx webpack-bundle-analyzer dist/my-app/stats.json
# Using source-map-explorer
npm install -g source-map-explorer
ng build --source-map
npx source-map-explorer dist/my-app/browser/*.js
Lazy Loading
Copy
// app.routes.ts - Lazy load feature modules
export const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'products',
loadChildren: () => import('./products/products.routes')
.then(m => m.PRODUCTS_ROUTES)
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.routes')
.then(m => m.ADMIN_ROUTES),
canMatch: [adminGuard]
}
];
// Component-level lazy loading
@Component({
template: `
@if (showEditor()) {
<ng-container *ngComponentOutlet="editorComponent()" />
}
`
})
export class DocumentComponent {
showEditor = signal(false);
editorComponent = signal<Type<any> | null>(null);
async loadEditor() {
const { RichTextEditorComponent } = await import('./rich-text-editor.component');
this.editorComponent.set(RichTextEditorComponent);
this.showEditor.set(true);
}
}
Deferrable Views (Angular 17+)
Copy
@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[]>();
}
Runtime Performance
OnPush Change Detection
Copy
@Component({
selector: 'app-product-card',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<article class="product-card">
<img [src]="product().image" [alt]="product().name" />
<h3>{{ product().name }}</h3>
<p class="price">{{ product().price | currency }}</p>
<button (click)="addToCart.emit(product())">Add to Cart</button>
</article>
`
})
export class ProductCardComponent {
product = input.required<Product>();
addToCart = output<Product>();
}
Signals for Fine-Grained Reactivity
Copy
@Component({
selector: 'app-dashboard',
template: `
<div class="dashboard">
<!-- Only re-renders when stats() changes -->
<app-stats-panel [stats]="stats()" />
<!-- Only re-renders when filteredItems() changes -->
<app-items-list [items]="filteredItems()" />
<!-- Only re-renders when totalPrice() changes -->
<app-price-summary [total]="totalPrice()" />
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DashboardComponent {
private itemsService = inject(ItemsService);
// Source signals
items = this.itemsService.items;
filter = signal<string>('');
sortBy = signal<'name' | 'price' | 'date'>('name');
// Computed signals - only recalculate when dependencies change
filteredItems = computed(() => {
const items = this.items();
const filterText = this.filter().toLowerCase();
return items
.filter(item => item.name.toLowerCase().includes(filterText))
.sort((a, b) => this.sortFn(a, b, this.sortBy()));
});
stats = computed(() => ({
total: this.items().length,
filtered: this.filteredItems().length,
avgPrice: this.calculateAverage(this.filteredItems())
}));
totalPrice = computed(() =>
this.filteredItems().reduce((sum, item) => sum + item.price, 0)
);
private sortFn(a: Item, b: Item, key: string): number {
return a[key] > b[key] ? 1 : -1;
}
private calculateAverage(items: Item[]): number {
if (items.length === 0) return 0;
return items.reduce((sum, i) => sum + i.price, 0) / items.length;
}
}
TrackBy for Lists
Copy
@Component({
template: `
<!-- ✅ With trackBy - only updates changed items -->
@for (item of items(); track item.id) {
<app-item-card [item]="item" />
}
<!-- Or with track expression -->
@for (item of items(); track trackById($index, item)) {
<app-item-card [item]="item" />
}
`
})
export class ItemListComponent {
items = input.required<Item[]>();
trackById(index: number, item: Item): string {
return item.id;
}
}
Virtual Scrolling
Copy
// Install CDK
// npm install @angular/cdk
import { CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll, CdkVirtualForOf } from '@angular/cdk/scrolling';
@Component({
selector: 'app-virtual-list',
standalone: true,
imports: [CdkVirtualScrollViewport, CdkFixedSizeVirtualScroll, CdkVirtualForOf],
template: `
<!-- Fixed size items -->
<cdk-virtual-scroll-viewport
itemSize="50"
class="viewport"
>
<div
*cdkVirtualFor="let item of items(); trackBy: trackById"
class="item"
>
{{ item.name }}
</div>
</cdk-virtual-scroll-viewport>
`,
styles: `
.viewport {
height: 500px;
width: 100%;
}
.item {
height: 50px;
display: flex;
align-items: center;
padding: 0 16px;
border-bottom: 1px solid #eee;
}
`
})
export class VirtualListComponent {
items = input.required<Item[]>();
trackById = (index: number, item: Item) => item.id;
}
// Auto-size virtual scroll (variable heights)
@Component({
template: `
<cdk-virtual-scroll-viewport
autosize
class="viewport"
>
<div
*cdkVirtualFor="let message of messages()"
class="message"
>
<img [src]="message.avatar" class="avatar" />
<div class="content">
<strong>{{ message.author }}</strong>
<p>{{ message.text }}</p>
</div>
</div>
</cdk-virtual-scroll-viewport>
`
})
export class ChatComponent {
messages = input.required<Message[]>();
}
Image Optimization
NgOptimizedImage Directive
Copy
import { NgOptimizedImage, provideImgixLoader } from '@angular/common';
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
provideImgixLoader('https://my-imgix-domain.imgix.net'),
// Or cloudinary
// provideCloudinaryLoader('https://res.cloudinary.com/my-account'),
// Or custom loader
// provideImageLoader((config) => `${config.src}?w=${config.width}`)
]
};
@Component({
imports: [NgOptimizedImage],
template: `
<!-- Optimized image with automatic srcset -->
<img
ngSrc="hero-image.jpg"
width="800"
height="600"
priority
placeholder
/>
<!-- Fill mode for responsive images -->
<div class="image-container">
<img
ngSrc="product.jpg"
fill
sizes="(max-width: 768px) 100vw, 50vw"
[loaderParams]="{ quality: 80 }"
/>
</div>
<!-- Lazy loaded (default) -->
<img
ngSrc="below-fold.jpg"
width="400"
height="300"
loading="lazy"
/>
`,
styles: `
.image-container {
position: relative;
width: 100%;
aspect-ratio: 16/9;
}
`
})
export class GalleryComponent {}
Memory Management
Subscription Cleanup
Copy
@Component({
template: `...`
})
export class DataComponent {
private destroyRef = inject(DestroyRef);
private dataService = inject(DataService);
data = signal<Data[]>([]);
ngOnInit() {
// Auto-cleanup with takeUntilDestroyed
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();
}
Web Workers for Heavy Computation
Copy
// app.worker.ts
addEventListener('message', ({ data }) => {
const result = heavyComputation(data);
postMessage(result);
});
function heavyComputation(data: number[]): number {
return data.reduce((sum, val) => sum + Math.pow(val, 2), 0);
}
// data-processor.service.ts
@Injectable({ providedIn: 'root' })
export class DataProcessorService {
private worker: Worker;
constructor() {
if (typeof Worker !== 'undefined') {
this.worker = new Worker(
new URL('./app.worker', import.meta.url)
);
}
}
processData(data: number[]): Observable<number> {
return new Observable(subscriber => {
this.worker.onmessage = ({ data }) => {
subscriber.next(data);
subscriber.complete();
};
this.worker.onerror = error => {
subscriber.error(error);
};
this.worker.postMessage(data);
});
}
}
// Usage in component
@Component({
template: `
<button (click)="calculate()">Calculate</button>
<p>Result: {{ result() }}</p>
`
})
export class CalculatorComponent {
private processor = inject(DataProcessorService);
result = signal<number | null>(null);
calculate() {
const largeDataSet = Array.from({ length: 1000000 }, () => Math.random());
this.processor.processData(largeDataSet).subscribe(
result => this.result.set(result)
);
}
}
Network Optimization
HTTP Caching
Copy
@Injectable({ providedIn: 'root' })
export class CachingInterceptor implements HttpInterceptor {
private cache = new Map<string, HttpResponse<any>>();
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (!this.isCacheable(req)) {
return next.handle(req);
}
const cachedResponse = this.cache.get(req.urlWithParams);
if (cachedResponse) {
return of(cachedResponse.clone());
}
return next.handle(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
this.cache.set(req.urlWithParams, event.clone());
}
})
);
}
private isCacheable(req: HttpRequest<any>): boolean {
return req.method === 'GET' && !req.headers.has('x-no-cache');
}
}
// Stale-while-revalidate pattern
@Injectable({ providedIn: 'root' })
export class StaleWhileRevalidateInterceptor implements HttpInterceptor {
private cache = new Map<string, { response: HttpResponse<any>; timestamp: number }>();
private maxAge = 60000; // 1 minute
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
const cached = this.cache.get(req.urlWithParams);
if (cached) {
const isStale = Date.now() - cached.timestamp > this.maxAge;
if (isStale) {
// Return stale, revalidate in background
next.handle(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
this.cache.set(req.urlWithParams, {
response: event.clone(),
timestamp: Date.now()
});
}
})
).subscribe();
return of(cached.response.clone());
}
return of(cached.response.clone());
}
return next.handle(req).pipe(
tap(event => {
if (event instanceof HttpResponse) {
this.cache.set(req.urlWithParams, {
response: event.clone(),
timestamp: Date.now()
});
}
})
);
}
}
Core Web Vitals
Copy
// web-vitals.service.ts
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';
@Injectable({ providedIn: 'root' })
export class WebVitalsService {
private analytics = inject(AnalyticsService);
trackWebVitals() {
onCLS(metric => this.reportMetric('CLS', metric));
onINP(metric => this.reportMetric('INP', metric));
onLCP(metric => this.reportMetric('LCP', metric));
onFCP(metric => this.reportMetric('FCP', metric));
onTTFB(metric => this.reportMetric('TTFB', metric));
}
private reportMetric(name: string, metric: Metric) {
this.analytics.track('Web Vital', {
name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id
});
}
}
Performance Checklist
Build Time
- Enable production mode
- AOT compilation
- Tree shaking enabled
- Bundle analysis < 200KB initial
Load Time
- Lazy load routes
- Defer heavy components
- Preload strategies
- Image optimization
Runtime
- OnPush everywhere
- Signals for state
- TrackBy for lists
- Virtual scrolling
Network
- HTTP caching
- Compression (gzip/brotli)
- CDN for assets
- Service Worker caching
Next: Content Projection
Master advanced content projection patterns