Performance Checklist β‘
A prioritized checklist for optimizing Angular application performance. Work through each section methodically β the items at the top of each section typically have the highest impact.
π Change Detection
Section titled βπ Change Detectionβ- Use
OnPushchange detection on every component β Prevents Angular from checking the component on every cycle. Only checks when inputs change or signals update.
@Component({ changeDetection: ChangeDetectionStrategy.OnPush, // ...})- Use signals for all component state β Signals notify Angular exactly which components need updating, skipping unnecessary checks.
// β
Signal-based statecount = signal(0);doubled = computed(() => this.count() * 2);
// β Plain properties trigger broad checkscount = 0;get doubled() { return this.count * 2; }-
Use
computed()for derived state β Computed signals are memoized and only recalculate when their dependencies change. -
Consider going zoneless β Angular 21 supports experimental zoneless change detection, eliminating Zone.js overhead entirely.
export const appConfig: ApplicationConfig = { providers: [ provideExperimentalZonelessChangeDetection(), // ... ],};- Avoid triggering change detection in
effect()β Effects that modify signals can cause infinite loops. Useuntracked()for reads you donβt want to track.
π¦ Bundle Size
Section titled βπ¦ Bundle Sizeβ- Lazy-load feature routes β Only load code when the user navigates to a route.
{ path: 'admin', loadChildren: () => import('./admin/admin.routes').then(m => m.adminRoutes),}- Use
loadComponentfor standalone page components β Avoids bundling pages the user may never visit.
{ path: 'settings', loadComponent: () => import('./settings.component').then(m => m.SettingsComponent),}-
Analyze your bundle β Use
ng build --stats-jsonand thennpx webpack-bundle-analyzer dist/stats.jsonto identify large dependencies. -
Remove unused imports β Tree-shaking only works if you donβt import things you donβt use. Check for barrel file re-exports that pull in entire modules.
-
Use
@deferfor heavy below-the-fold components β Deferred components are split into separate chunks automatically. -
Avoid importing entire libraries β Import specific functions/modules instead of entire packages.
// β
Good β tree-shakable importimport { map, filter } from 'rxjs';
// β Bad β imports everythingimport * as rxjs from 'rxjs';πΌοΈ Rendering Performance
Section titled βπΌοΈ Rendering Performanceβ- Always provide
trackin@forblocks β Thetrackexpression enables Angular to reuse DOM nodes instead of recreating them.
<!-- β
Track by unique ID -->@for (user of users(); track user.id) { <app-user-card [user]="user" />}
<!-- β οΈ Track by index β only if items have no unique ID -->@for (item of items(); track $index) { <span>{{ item }}</span>}- Use
@deferto lazy-load heavy components β Split large widgets, charts, and editors into deferred blocks.
@defer (on viewport) { <app-analytics-chart [data]="chartData()" />} @placeholder { <div class="chart-skeleton" style="height: 400px"></div>}- Use virtual scrolling for long lists β The CDK
VirtualScrollViewportrenders only visible items.
<cdk-virtual-scroll-viewport itemSize="48" class="list-viewport"> <div *cdkVirtualFor="let item of items" class="list-item"> {{ item.name }} </div></cdk-virtual-scroll-viewport>-
Avoid DOM manipulation in tight loops β Batch DOM changes. Prefer signal-driven updates over imperative DOM mutations.
-
Limit the use of
effect()β Effects run on every signal change. Prefercomputed()when youβre just deriving values.
π Network Performance
Section titled βπ Network Performanceβ- Use
httpResourcefor declarative data fetching β Caches results and provides built-in loading/error states.
readonly users = httpResource<User[]>(() => '/api/users');- Implement HTTP caching with interceptors β Cache GET requests to avoid redundant network calls.
export const cacheInterceptor: HttpInterceptorFn = (req, next) => { if (req.method !== 'GET') return next(req);
const cache = inject(HttpCacheService); const cached = cache.get(req.urlWithParams); if (cached) return of(cached);
return next(req).pipe( tap(event => { if (event instanceof HttpResponse) { cache.set(req.urlWithParams, event); } }), );};- Use preloading strategies β Preload lazy routes after the initial load completes.
provideRouter( routes, withPreloading(PreloadAllModules),)- Use service workers for offline support β Add
@angular/service-workerfor PWA caching.
ng add @angular/service-worker- Compress API responses β Ensure your server sends gzip or brotli-compressed responses.
πΌοΈ Image Optimization
Section titled βπΌοΈ Image Optimizationβ- Use
NgOptimizedImagefor all images β Enforces best practices automatically (lazy loading, sizing, priority hints).
<!-- Hero image β mark as priority --><img ngSrc="/hero.jpg" width="1200" height="600" priority />
<!-- Regular images β lazy-loaded by default --><img ngSrc="/product.jpg" width="400" height="300" />-
Set
priorityon LCP images β The Largest Contentful Paint image should always have thepriorityattribute. -
Provide
widthandheightattributes β Prevents layout shift (CLS).NgOptimizedImagewill warn if these are missing. -
Use responsive images with
ngSrcsetβ Serve appropriate image sizes based on viewport.
<img ngSrc="/product.jpg" ngSrcset="300w, 600w, 900w" sizes="(max-width: 768px) 100vw, 50vw" width="900" height="600"/>- Use modern image formats β Serve WebP or AVIF with fallbacks.
π§Ή Memory Management
Section titled βπ§Ή Memory Managementβ- Use
takeUntilDestroyed()for all subscriptions β Automatically unsubscribes when the component is destroyed.
export class MyComponent { private readonly destroyRef = inject(DestroyRef);
constructor() { this.someObservable$ .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(value => this.data.set(value)); }}-
Prefer signals over manual subscriptions β Signals donβt need unsubscription. Use
computed()andeffect()instead of.subscribe()where possible. -
Clean up event listeners β Use the
hostproperty in the decorator, which Angular manages automatically.
@Component({ host: { '(window:resize)': 'onResize($event)', },})-
Avoid memory leaks in
effect()β Effects are cleaned up automatically when the owning context is destroyed. Donβt create effects in services without proper lifecycle management. -
Use
untracked()to prevent unnecessary subscriptions β In effects that read many signals, useuntracked()for signals you only want to read, not react to.
effect(() => { const id = this.userId(); // react to this const config = untracked(() => this.config()); // just read this this.loadUser(id, config);});ποΈ Build Optimization
Section titled βποΈ Build Optimizationβ-
Use the default esbuild builder β Angular 21 uses esbuild by default, which is significantly faster than webpack.
-
Enable production optimizations β Always build with
ng build(production mode is the default).
ng build # production by defaultng build --configuration=development # dev build-
Enable route-level code splitting β This happens automatically with
loadComponentandloadChildren. -
Configure budget limits β Set bundle size budgets in
angular.jsonto catch regressions.
"budgets": [ { "type": "initial", "maximumWarning": "500kB", "maximumError": "1MB" }, { "type": "anyComponentStyle", "maximumWarning": "4kB", "maximumError": "8kB" }]- Enable SSR/SSG for content pages β Server-side rendering improves Time to First Byte (TTFB) and SEO.
ng add @angular/ssrπ Monitoring and Profiling
Section titled βπ Monitoring and Profilingβ-
Use Angular DevTools β The Chrome/Firefox extension shows component trees, change detection cycles, and profiler data.
-
Profile with Chrome DevTools Performance tab β Record and analyze runtime performance bottlenecks.
-
Monitor Core Web Vitals β Track LCP, FID/INP, and CLS in production.
-
Set up error tracking β Use tools like Sentry to catch runtime errors in production.