Minko Gechev: Angular Performance
Minko Gechev's core belief: Performance is a feature, not an afterthought. Every millisecond of load time and every kilobyte of bundle size affects user experience.
The Foundational Principle
"The fastest code is code that doesn't run. The smallest bundle is the one you don't ship."
Performance optimization in Angular is about:
- Shipping less JavaScript
- Running less change detection
- Loading only what's needed
- Measuring before optimizing
Core Principles
1. Lazy Loading Is Non-Negotiable
Every feature module should be lazy loaded unless proven otherwise.
Not this:
// app.module.ts - everything loaded upfront
@NgModule({
imports: [
UsersModule,
AdminModule,
ReportsModule,
SettingsModule // User may never visit settings
]
})
class AppModule {}
This:
// app-routing.module.ts - load on demand
const routes: Routes = [
{ path: 'users', loadChildren: () => import('./users/users.module').then(m => m.UsersModule) },
{ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) },
{ path: 'reports', loadChildren: () => import('./reports/reports.module').then(m => m.ReportsModule) },
{ path: 'settings', loadChildren: () => import('./settings/settings.module').then(m => m.SettingsModule) }
];
With standalone components (Angular 14+):
const routes: Routes = [
{
path: 'dashboard',
loadComponent: () => import('./dashboard.component').then(c => c.DashboardComponent)
}
];
2. OnPush Change Detection Strategy
Default change detection checks every component on every event. OnPush checks only when inputs change.
Not this:
@Component({
// Default change detection - checks on every click anywhere
})
class ExpensiveListComponent {
@Input() items: Item[];
}
This:
@Component({
changeDetection: ChangeDetectionStrategy.OnPush // Only checks when items reference changes
})
class ExpensiveListComponent {
@Input() items: Item[];
}
Rules for OnPush:
- All
@Input()must be immutable (new reference on change) - Use
asyncpipe for observables (triggers change detection correctly) - Call
ChangeDetectorRef.markForCheck()only when truly needed
3. TrackBy for All ngFor
Without trackBy, Angular destroys and recreates DOM for every change.
Not this:
<li *ngFor="let user of users">{{ user.name }}</li>
<!-- Entire list re-rendered when any user changes -->
This:
<li *ngFor="let user of users; trackBy: trackByUserId">{{ user.name }}</li>
<!-- Only changed items re-rendered -->
trackByUserId(index: number, user: User): string {
return user.id;
}
4. Async Pipe Over Manual Subscriptions
Manual subscriptions leak memory. Async pipe handles cleanup.
Not this:
class UserComponent implements OnInit, OnDestroy {
user: User;
private subscription: Subscription;
ngOnInit() {
this.subscription = this.userService.getUser().subscribe(u => this.user = u);
}
ngOnDestroy() {
this.subscription.unsubscribe(); // Easy to forget
}
}
This:
class UserComponent {
user$ = this.userService.getUser();
constructor(private userService: UserService) {}
}
<div *ngIf="user$ | async as user">{{ user.name }}</div>
5. Bundle Analysis and Budgets
Set budgets, enforce them, analyze violations.
angular.json:
{
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
]
}
Analyze bundles:
ng build --stats-json
npx webpack-bundle-analyzer dist/stats.json
6. Preloading Strategies
Don't just lazy load—preload intelligently.
// Preload all modules after initial load
@NgModule({
imports: [RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules
})]
})
// Or custom strategy - preload based on user behavior
@Injectable()
class CustomPreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: () => Observable<any>): Observable<any> {
return route.data?.['preload'] ? load() : of(null);
}
}
7. Virtual Scrolling for Large Lists
Never render thousands of DOM nodes. Use virtual scrolling.
import { ScrollingModule } from '@angular/cdk/scrolling';
@Component({
template: `
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
<div *cdkVirtualFor="let item of items" class="item">
{{ item.name }}
</div>
</cdk-virtual-scroll-viewport>
`
})
class LargeListComponent {
items = Array.from({ length: 10000 }, (_, i) => ({ name: `Item ${i}` }));
}
8. Web Workers for Heavy Computation
Offload CPU-intensive work to avoid blocking the main thread.
// app.worker.ts
addEventListener('message', ({ data }) => {
const result = heavyComputation(data);
postMessage(result);
});
// component.ts
if (typeof Worker !== 'undefined') {
const worker = new Worker(new URL('./app.worker', import.meta.url));
worker.onmessage = ({ data }) => {
this.result = data;
};
worker.postMessage(this.inputData);
}
9. Image Optimization with NgOptimizedImage
Angular's built-in image optimization directive.
import { NgOptimizedImage } from '@angular/common';
@Component({
imports: [NgOptimizedImage],
template: `
<img ngSrc="hero.jpg" width="800" height="600" priority>
<img ngSrc="thumbnail.jpg" width="200" height="150" loading="lazy">
`
})
Benefits:
- Automatic lazy loading
- Prevents layout shift (requires width/height)
- Preconnect hints for CDNs
- Warning for LCP images without
priority
10. Defer Blocks (Angular 17+)
Declarative lazy loading in templates.
@defer (on viewport) {
<heavy-component />
} @placeholder {
<lightweight-skeleton />
} @loading (minimum 500ms) {
<spinner />
}
Trigger options:
on viewport- when enters viewporton idle- when browser is idleon interaction- on click/focuson hover- on mouse hoveron timer(500ms)- after delay
The Gechev Test
Before shipping, ask:
- Is everything lazy loaded? Feature modules, standalone components?
- Is OnPush used everywhere possible? With immutable inputs?
- Do all ngFor have trackBy? No exceptions?
- Am I within bundle budgets? Have I analyzed what's in the bundle?
- Are large lists virtualized? Or paginated?
- Is heavy computation off main thread? Web workers for CPU work?
- Are images optimized? Using NgOptimizedImage?
When Reviewing Code
Apply these checks:
- Lazy loading for all feature modules
-
ChangeDetectionStrategy.OnPushon components -
trackByon all*ngFor -
asyncpipe instead of manual subscriptions - Bundle budgets configured and passing
- Virtual scrolling for lists > 100 items
- Images use
NgOptimizedImage - No synchronous heavy computation in components
- Preloading strategy configured
Performance Debugging
// Enable Angular DevTools profiler
// In browser: Angular DevTools extension
// Measure change detection
import { enableDebugTools } from '@angular/platform-browser';
enableDebugTools(appRef.components[0]);
// Then in console: ng.profiler.timeChangeDetection()
// Trace what triggers change detection
constructor(private ngZone: NgZone) {
ngZone.onStable.subscribe(() => console.log('CD cycle complete'));
}
When NOT to Use This Skill
Use a different skill when:
- Designing component architecture → Use
angular-core(DI, testability) - Applying design patterns → Use
design-patterns - General code clarity → Use
clarity - Type system design → Use
typescript(TypeScript)
Minko Gechev is the Angular performance skill—use it for optimization, lazy loading, and runtime efficiency.
Sources
- Gechev, "Angular Performance Checklist" (GitHub)
- Angular documentation - Performance section
- web.dev Core Web Vitals guidance
- Gechev's conference talks on Angular performance
"Measure first. Optimize what matters. Ship less JavaScript." — Minko Gechev
