TL;DR: Angular apps often suffer sluggish performance due to inefficient change detection and bloated bundles. This guide explores 10 cutting-edge Angular performance optimization techniques, including signals, zoneless architecture, and lazy loading, to help developers build faster, more responsive applications.
Why Angular performance matters in 2025?
Angular apps often suffer from large bundle sizes and inefficient lazy loading, leading to sluggish first paint and poor user experience.
Angular introduces revolutionary performance improvements that fundamentally change how we build fast applications. With zoneless change detection now stable, enhanced signal-based reactivity, and advanced tree-shaking capabilities, Angular has never been more performant.
Whether you’re migrating from previous versions of Angular or starting fresh, these 10 cutting-edge techniques leverage Angular’s latest features to create blazingly fast applications that excel in Core Web Vitals and provide exceptional user experiences.
1. Go zoneless with change detection
Zoneless change detection is Angular’s most significant performance breakthrough, eliminating Zone.js and providing fine-grained control over when change detection runs.
Why it matters
Zone.js adds significant overhead in payload size (~30KB), startup time, and runtime performance. It triggers change detection unnecessarily since it cannot determine if the application state actually changed. Zoneless change detection runs only when Angular is explicitly notified of changes.
How to apply Zoneless change detection
Step 1: Enable zoneless change detection in your main.ts
file.
// main.ts - Angular standalone bootstrap
import { bootstrapApplication } from "@angular/platform-browser";
import { provideZonelessChangeDetection } from "@angular/core";
bootstrapApplication(AppComponent, {
providers: [
provideZonelessChangeDetection(),
// other providers
],
});
Step 2: Remove Zone.js
from your build.
// angular.json - Remove zone.js from polyfills
{
"build": {
"options": {
"polyfills": [
// Remove "zone.js" from here
]
}
},
"test": {
"options": {
"polyfills": [
// Remove "zone.js/testing" from here
]
}
}
}
Step 3: Uninstall Zone.js dependency by running this command:
npm uninstall zone.js
Step 4: Update components for zoneless compatibility:
import {
Component,
ChangeDetectionStrategy,
inject,
signal,
} from "@angular/core";
@Component({
selector: "app-user-profile",
changeDetection: ChangeDetectionStrategy.OnPush, // Recommended for zoneless
template: `
<div class="profile">
<h2>{{ userData().name }}</h2>
<p>{{ userData().email }}</p>
<button (click)="updateProfile()">Update</button>
</div>
`,
})
export class UserProfileComponent {
userData = signal({ name: "John Doe", email: "john@example.com" });
updateProfile() {
// Signal updates automatically notify Angular of changes
this.userData.update((user) => ({
...user,
name: "Updated Name",
}));
}
}
Gotchas to watch out for
- Components must use
OnPush-compatible
patterns - Remove
NgZone.onMicrotaskEmpty
,NgZone.onUnstable
, andNgZone.isStable
usage - Use
afterNextRender
orafterEveryRender
instead of Zone-based timing - Third-party libraries may need compatibility checks
Performance Impact
Zoneless change detection delivers significant performance improvements:
- Approximately 30KB smaller bundle size
- 40–60% faster startup time
- 50–70% reduction in change detection overhead
- Improved Core Web Vitals scores
2. Prevent zone pollution with strategic NgZone usage
Zone pollution occurs when third-party libraries or inefficient code trigger unnecessary change detection cycles. Understanding zone management helps optimize mixed environments and library integrations even in zoneless applications.
Why it matters
Unnecessary change detection calls can degrade performance by 30-50%. Angular DevTools often shows consecutive change detection bars caused by setTimeout
, setInterval
, requestAnimationFrame
, or event handlers from third-party libraries.
How to apply
Step 1: Run third-party library code outside the Angular zone:
import {Component, NgZone, OnInit, inject, signal} from "@angular/core";
import * as Plotly from "plotly.js-dist-min";
@Component({
selector: "app-data-visualization",
template: `
<div id="plotly-chart"></div>
<button (click)="updateChart()">Update Data</button>
`,
})
export class DataVisualizationComponent implements OnInit {
private ngZone = inject(NgZone);
chartData = signal<any[]>([]);
ngOnInit() {
// Initialize third-party library outside Angular zone
this.ngZone.runOutsideAngular(() => {
this.initializePlotly();
});
}
private async initializePlotly() {
const plotly = await Plotly.newPlot("plotly-chart", this.chartData());
// Event handlers run outside Angular zone
plotly.on("plotly_click", (event: any) => {
// Re-enter Angular zone when updating app state
this.ngZone.run(() => {
this.chartData.update((data) => [...data, event]);
});
});
}
updateChart() {
// Animations and timers outside Angular zone
this.ngZone.runOutsideAngular(() => {
setInterval(() => {
this.updateVisualization();
}, 1000);
});
}
private updateVisualization() {
// Chart update logic
}
}
Step 2: Handle polling and timers efficiently:
@Component({
selector: "app-real-time-data",
template: `
<div>Last Update: {{ lastUpdate() }}</div>
<div>Data: {{ liveData() | json }}</div>
`,
})
export class RealTimeDataComponent implements OnInit, OnDestroy {
private ngZone = inject(NgZone);
private intervalId?: number;
lastUpdate = signal(new Date());
liveData = signal<any>({});
ngOnInit() {
this.ngZone.runOutsideAngular(() => {
this.intervalId = setInterval(() => {
this.fetchData().then((data) => {
// Re-enter zone only when updating state
this.ngZone.run(() => {
this.liveData.set(data);
this.lastUpdate.set(new Date());
});
});
}, 5000);
});
}
ngOnDestroy() {
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
private async fetchData() {
return fetch("/api/live-data").then((r) => r.json());
}
}
Gotchas to watch out for
-
NgZone.run
andNgZone.runOutsideAngular
remain compatible with zoneless applications - Don’t remove these calls when migrating to zoneless – they provide performance benefits
- Use Angular DevTools profiler to identify zone pollution patterns
Performance impact
Significant improvements:
- 25-40% reduction in unnecessary change detection cycles
- Smoother animations and interactions
- Better battery life on mobile devices
3. Use Angular Signals for reactive state
Signals are Angular’s new reactive primitive. They provide fine-grained reactivity and automatic dependency tracking, preparing your app for zoneless change detection.
Why it matters
Signals eliminate the need for manual subscription management and provide more predictable updates compared to traditional observables in templates.
How to apply
Step 1: Replace observables with signals:
import { Component, signal, computed } from "@angular/core";
@Component({
selector: "app-counter",
template: `
<div>
<p>Count: {{ count() }}</p>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">+</button>
</div>
`,
})
export class CounterComponent {
count = signal(0);
doubleCount = computed(() => this.count() * 2);
increment() {
this.count.update((value) => value + 1);
}
}
Step 2: Use signals with the OnPush strategy:
@Component({
selector: "app-data-display",
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
@for (item of items()) {
<div>{{ item.name }}</div>
}
`,
})
export class DataDisplayComponent {
items = signal<Item[]>([]);
addItem(item: Item) {
this.items.update((items) => [...items, item]);
}
}
Gotchas to watch out for
- Migration from observables should be gradual
- Not all Angular APIs support signals yet
Performance impact: Reduces unnecessary re-renders and prepares for zoneless Angular.
4. Master advanced lazy loading with modern syntax
What it is
Angular’s enhanced lazy loading supports more granular code splitting, intelligent preloading strategies, and seamless integration with standalone components and signals.
Why it matters
Modern applications can exceed 10MB in bundle size. Strategic lazy loading can reduce initial load by 70-85% while intelligent preloading ensures instant navigation when users need it.
How to apply
Step 1: Master standalone component lazy loading:
// app.routes.ts
import {Routes} from "@angular/router";
export const routes: Routes = [
{
path: "",
redirectTo: "/dashboard",
pathMatch: "full",
},
{
path: "dashboard",
loadComponent: () =>
import("./features/dashboard/dashboard.component").then(
(m) => m.DashboardComponent
),
data: { preload: true }, // Mark for preloading
},
{
path: "admin",
loadChildren: () =>
import("./features/admin/admin.routes").then((m) => m.adminRoutes),
canMatch: [() => inject(AuthService).isAdmin()], // Guard integration
data: { preload: false },
},
{
path: "reports",
loadComponent: () =>
import("./features/reports/reports.component").then(
(m) => m.ReportsComponent
),
providers: [
// Route-level providers
importProvidersFrom(ChartsModule),
{ provide: CHART_CONFIG, useValue: chartConfig },
],
},
];
Step 2: Implement intelligent preloading strategies:
// smart-preloading.strategy.ts
import { PreloadingStrategy, Route } from "@angular/router";
import { Injectable } from "@angular/core";
import { Observable, of, timer } from "rxjs";
import { switchMap } from "rxjs/operators";
@Injectable()
export class SmartPreloadingStrategy implements PreloadingStrategy {
preload(route: Route, load: Function): Observable<any> {
return this.shouldPreload(route).pipe(
switchMap((shouldPreload) => (shouldPreload ? load() : of(null)))
);
}
private shouldPreload(route: Route): Observable<boolean> {
// Don't preload if user has slow connection
if (!this.isFastConnection()) {
return of(false);
}
// Preload based on route data
if (route.data?.["preload"] === true) {
return of(true);
}
// Delay preloading for less critical routes
if (route.data?.["preload"] === "delayed") {
return timer(3000).pipe(switchMap(() => of(true)));
}
// Default: don't preload
return of(false);
}
private isFastConnection(): boolean {
// Check if Network Information API is available
const connection =
(navigator as any).connection ||
(navigator as any).mozConnection ||
(navigator as any).webkitConnection;
if (!connection) {
return true; // Assume fast connection if API not available
}
// Consider connection fast if 4G or better, or good download speed
return (
connection.effectiveType === "4g" ||
connection.downlink >= 2 ||
connection.effectiveType === "5g"
);
}
}
// app.config.ts
import { ApplicationConfig } from "@angular/core";
import { provideRouter, withPreloading } from "@angular/router";
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withPreloading(SmartPreloadingStrategy)),
SmartPreloadingStrategy,
],
};
Step 3: Optimize bundle splitting with strategic imports:
// feature.component.ts - Conditional feature loading
import { Component, signal } from "@angular/core";
@Component({
selector: "app-analytics-dashboard",
template: `
<div class="dashboard">
<h2>Analytics Dashboard</h2>
@if (showAdvancedCharts()) {
<app-advanced-charts [data]="chartData()"></app-advanced-charts>
} @else {
<button (click)="loadAdvancedCharts()">Load Advanced Charts</button>
} @defer (on interaction(exportBtn)) {
<app-export-tools></app-export-tools>
} @placeholder {
<button #exportBtn>Load Export Tools</button>
}
</div>
`,
})
export class AnalyticsDashboardComponent {
showAdvancedCharts = signal(false);
chartData = signal<ChartData[]>([
{ id: 1, name: "Sales", value: 1000 },
{ id: 2, name: "Revenue", value: 5000 },
]);
async loadAdvancedCharts() {
// Dynamic import for heavy chart library
const { AdvancedChartsModule } = await import(
"./charts/advanced-charts.module"
);
// Set flag to show the component
this.showAdvancedCharts.set(true);
}
}
Gotchas to watch out for
- Avoid circular dependencies in lazy-loaded modules
- Test preloading strategies on various network conditions
- Monitor actual vs expected bundle sizes with tools like webpack-bundle-analyzer
- Consider user behavior patterns when implementing preloading
Performance Impact
Dramatic improvements:
- 70-85% reduction in initial bundle size
- 40-60% faster Time to Interactive (TTI)
- Intelligent preloading reduces perceived navigation time by 80%
- Better Core Web Vitals scores across all metrics
5. Leverage deferrable views for smart rendering
What it is
Deferrable views provide sophisticated conditional rendering with improved triggers, better error handling, and seamless integration with signals and zoneless change detection.
Why it matters
Deferrable views can improve Initial Bundle Size by 30-50% and Largest Contentful Paint (LCP) by 25-40% by strategically delaying non-critical content rendering.
How to apply
Step 1: Master advanced deferrable view patterns:
// feature.component.ts - Conditional feature loading
import { Component, signal } from "@angular/core";
@Component({
selector: "app-analytics-dashboard",
template: `
<div class="dashboard">
<h2>Analytics Dashboard</h2>
@if (showAdvancedCharts()) {
<app-advanced-charts [data]="chartData()"></app-advanced-charts>
} @else {
<button (click)="loadAdvancedCharts()">Load Advanced Charts</button>
} @defer (on interaction(exportBtn)) {
<app-export-tools></app-export-tools>
} @placeholder {
<button #exportBtn>Load Export Tools</button>
}
</div>
`,
})
export class AnalyticsDashboardComponent {
showAdvancedCharts = signal(false);
chartData = signal<ChartData[]>([
{ id: 1, name: "Sales", value: 1000 },
{ id: 2, name: "Revenue", value: 5000 },
]);
async loadAdvancedCharts() {
// Dynamic import for heavy chart library
const { AdvancedChartsModule } = await import(
"./charts/advanced-charts.module"
);
// Set flag to show the component
this.showAdvancedCharts.set(true);
}
}
Step 2: Implement progressive enhancement with defer blocks:
@Component({
selector: "app-product-catalog",
template: `
<div class="product-catalog">
<!-- Essential products load immediately -->
@for (product of essentialProducts(); track product.id) {
<app-product-card [product]="product"></app-product-card>
}
<!-- Advanced filtering deferred until needed -->
@defer (when showAdvancedFilters()) {
<app-advanced-filters (filtersChanged)="applyFilters($event)">
</app-advanced-filters>
} @placeholder {
<button (click)="showAdvancedFilters.set(true)">
Show Advanced Filters
</button>
}
<!-- Load more on viewport -->
@defer (on viewport(loadMoreTrigger)) {
@for (product of additionalProducts(); track product.id) {
<app-product-card [product]="product"></app-product-card>
}
}
@placeholder {
<button #loadMoreTrigger>Load More Products</button>
}
<!-- Premium banner -->
@defer (on timer(3s)) {
<app-premium-banner></app-premium-banner>
}
</div>
`,
})
export class ProductCatalogComponent {
essentialProducts = signal<Product[]>([
{ id: 1, name: "Laptop", price: 999 },
{ id: 2, name: "Phone", price: 599 },
]);
additionalProducts = signal<Product[]>([
{ id: 3, name: "Tablet", price: 399 },
{ id: 4, name: "Watch", price: 299 },
]);
showAdvancedFilters = signal(false);
applyFilters(filters: FilterOptions) {
// Apply filtering logic
console.log("Applying filters:", filters);
}
}
Gotchas to watch out for
- Defer blocks must contain standalone components
- Avoid referencing deferred components in parent component logic
- Test defer conditions thoroughly across different user scenarios
- Monitor the Core Web Vitals impact of defer strategies
Performance Impact
Substantial benefits:
- 30-50% reduction in initial bundle size
- 25-40% improvement in Largest Contentful Paint (LCP)
- 60-80% reduction in Time to Interactive for complex pages
- Enhanced perceived performance through smart loading patterns
6. Optimize images with NgOptimizedImage
What it is
Angular’s NgOptimizedImage
directive provides automatic image optimization, responsive loading, and Core Web Vitals improvements.
Why it matters
Images often represent the largest assets in web applications and significantly impact Largest Contentful Paint (LCP) and Cumulative Layout Shift (CLS) metrics.
How to apply
Step 1: Import and use NgOptimizedImage
:
import { NgOptimizedImage } from "@angular/common";
@Component({
imports: [NgOptimizedImage],
template: `
<img
ngSrc="hero-banner.jpg"
width="1200"
height="600"
priority
alt="Hero banner"
/>
`,
})
export class HeroComponent {}
Step 2: Configure responsive images:
@Component({
template: `
<img
ngSrc="product-image.jpg"
width="400"
height="300"
sizes="(max-width: 768px) 100vw, 50vw"
placeholder
alt="Product image"
/>
`,
})
export class ProductComponent {}
Step 3: Set up image loader for CDN:
// app.config.ts
import { IMAGE_LOADER, ImageLoaderConfig } from "@angular/common";
export const appConfig: ApplicationConfig = {
providers: [
{
provide: IMAGE_LOADER,
useValue: (config: ImageLoaderConfig) => {
return `https://your-cdn.com/images/${config.src}?w=${config.width}`;
},
},
],
};
Gotchas to watch out for
- Always include width and height to prevent layout shifts
- Use the priority attribute for above-the-fold images
- Keep placeholder images under 4KB
Performance Impact: Can improve LCP by 20-40% and eliminate layout shifts.
7. Implement advanced SSR with PendingTasks API
What it is
Angular’s enhanced SSR leverages the PendingTasks API for precise control over application serialization, enabling better hydration strategies and optimal Core Web Vitals.
Why it matters
SSR with proper task management can improve First Contentful Paint (FCP) by 40-60% and Largest Contentful Paint (LCP) by 30-50%, while preventing hydration mismatches that cause layout shifts.
How to apply
Step 1: Configure advanced SSR with hydration optimization:
// main.server.ts
import { bootstrapApplication } from "@angular/platform-browser";
import { provideServerRendering } from "@angular/platform-server";
import {
provideClientHydration,
withEventReplay,
} from "@angular/platform-browser";
import { provideZonelessChangeDetection } from "@angular/core";
import { provideRouter } from "@angular/router";
import { AppComponent } from "./app.component";
import { routes } from "./app.routes";
export const appConfig = {
providers: [
provideZonelessChangeDetection(),
provideRouter(routes),
provideClientHydration(withEventReplay()), // Replay user events during hydration
provideServerRendering(),
// Additional providers for your app
],
};
export default () => bootstrapApplication(AppComponent, appConfig);
Step 2: Master PendingTasks
API for precise SSR control:
// data-loading.service.ts
import { Injectable, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { PendingTasks } from "@angular/core";
import { pendingUntilEvent } from "@angular/core/rxjs-interop";
import { catchError, of, finalize } from "rxjs";
import { Observable } from "rxjs";
@Injectable({ providedIn: "root" })
export class DataLoadingService {
private http = inject(HttpClient);
private pendingTasks = inject(PendingTasks);
// Return observables - let components manage subscriptions and signals
getCriticalData(): Observable<any[]> {
const taskCleanup = this.pendingTasks.add();
return this.http.get<any[]>("/api/critical-data").pipe(
catchError(() => of([])), // Fallback data on error
pendingUntilEvent(), // Keeps app "unstable" until complete
finalize(() => taskCleanup()) // Clean up task when observable completes
);
}
// Return observable for secondary data
getSecondaryData(): Observable<any[]> {
return this.http.get<any[]>("/api/secondary-data").pipe(
catchError(() => of([])), // Fallback data on error
pendingUntilEvent() // Keeps app "unstable" until complete
);
}
}
Step 3: Implement hydration-aware components:
import {
Component,
OnInit,
inject,
signal,
PLATFORM_ID,
DestroyRef,
} from "@angular/core";
import { isPlatformBrowser } from "@angular/common";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { DataLoadingService } from "./data-loading.service";
@Component({
selector: "app-ssr-optimized",
template: `
<div class="ssr-container">
<!-- Critical content rendered server-side -->
<h1>{{ pageTitle() }}</h1>
@for (item of criticalData(); track item.id) {
<div class="critical-item">{{ item.title }}</div>
}
<!-- Non-critical content deferred until hydration -->
@defer (on idle) {
<h2>Additional Information</h2>
@for (item of secondaryData(); track item.id) {
<div class="secondary-item">{{ item.description }}</div>
}
}
@placeholder {
<div>Loading additional content...</div>
}
<!-- Interactive features loaded after hydration -->
@if (isHydrated()) {
<app-user-interactions></app-user-interactions>
}
</div>
`,
})
export class SsrOptimizedComponent implements OnInit {
private dataService = inject(DataLoadingService);
private platformId = inject(PLATFORM_ID);
private destroyRef = inject(DestroyRef);
pageTitle = signal("SSR Optimized Page");
criticalData = signal<any[]>([]);
secondaryData = signal<any[]>([]);
isHydrated = signal(false);
ngOnInit() {
// Always load critical data (needed for SSR)
this.dataService
.getCriticalData()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((data) => this.criticalData.set(data));
// Only load secondary data and mark as hydrated in browser
if (isPlatformBrowser(this.platformId)) {
this.isHydrated.set(true);
this.dataService
.getSecondaryData()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((data) => this.secondaryData.set(data));
}
}
}
Gotchas to watch out for
- Use PendingTasks instead of Zone-based timing APIs
- Ensure server and client render the same initial content
- Test hydration behavior on slow devices and connections
- Monitor hydration performance in production
Performance Impact
Outstanding improvements:
- 40-60% improvement in First Contentful Paint (FCP)
- 30-50% improvement in Largest Contentful Paint (LCP)
- Elimination of hydration-related Cumulative Layout Shift
- Better SEO rankings through consistent server rendering
8. Strategic bundle optimization with Tree-Shaking
What it is
Angular’s enhanced tree-shaking capabilities, strategic code organization, and modern build optimizations can dramatically reduce bundle sizes through better dead code elimination.
Why it matters
Every 100KB of JavaScript adds 1-2 seconds to mobile device startup time. Modern bundle optimization can reduce final bundle sizes by 40-70% while maintaining functionality.
How to apply
Step 1: Optimize imports for maximum tree-shaking:
// ❌ Poor tree-shaking - imports entire libraries
import * as _ from "lodash";
import * as moment from "moment";
// ✅ Excellent tree-shaking - specific imports only
import { debounceTime, distinctUntilChanged } from "rxjs/operators";
import { isEqual, cloneDeep } from "lodash-es"; // Use ES modules version
import { format, parseISO } from "date-fns"; // Lightweight alternative
@Injectable()
export class OptimizedDataService {
private deepClone<T>(obj: T): T {
return cloneDeep(obj); // Tree-shakable lodash function
}
formatDate(date: Date): string {
return format(date, "yyyy-MM-dd"); // Tree-shakable date-fns function
}
}
Step 2: Implement a strategic enum and constant organization:
// ❌ Large monolithic enums that prevent tree-shaking
export enum AppConstants {
API_ENDPOINTS: { ... }, // 50 endpoints
ERROR_MESSAGES: { ... }, // 100 messages
UI_CONSTANTS: { ... }, // 50 UI values
}
// ✅ Granular, tree-shakable constants
// auth/constants.ts
export const AUTH_ENDPOINTS = {
LOGIN: '/api/auth/login',
LOGOUT: '/api/auth/logout',
} as const;
export const AUTH_ERRORS = {
INVALID_CREDENTIALS: 'Invalid username or password',
SESSION_EXPIRED: 'Your session has expired'
} as const;
// Only import what you actually use
import { AUTH_ENDPOINTS, AUTH_ERRORS } from './auth/constants';
// AUTH_ERRORS won't be bundled if not imported
@Injectable({ providedIn: 'root' })
export class AuthService {
private http = inject(HttpClient);
login(credentials: any) {
return this.http.post(AUTH_ENDPOINTS.LOGIN, credentials);
}
logout() {
return this.http.post(AUTH_ENDPOINTS.LOGOUT, {});
}
}
Step 3: Optimize service injection and dependencies:
// ❌ Heavy facade service that pulls in everything
@Injectable()
export class AppFacadeService {
constructor(
private userService: UserService,
private orderService: OrderService,
private paymentService: PaymentService,
private analyticsService: AnalyticsService // 10+ more services...
) {}
}
// ✅ Focused, lightweight services with minimal dependencies
@Injectable()
export class UserProfileService {
private http = inject(HttpClient);
private router = inject(Router);
getUserProfile(id: string) {
return this.http.get<UserProfile>(`/api/users/${id}`);
}
}
@Injectable()
export class OrderManagementService {
private http = inject(HttpClient);
private paymentService = inject(PaymentService); // Only when needed
processOrder(order: Order) {
// Specific functionality with minimal deps
}
}
Step 4: Implement advanced code splitting strategies:
// feature-loader.service.ts
import { Injectable, signal } from "@angular/core";
@Injectable({ providedIn: "root" })
export class FeatureLoaderService {
featureFlags = signal({ analytics: false, premiumCharts: false });
// Load features based on user tier
loadChartLibrary(userTier: "basic" | "premium") {
return userTier === "premium"
? import(/* "premium-charts" */ "./charts/d3-charts")
: import(/* "basic-charts" */ "./charts/canvas-charts");
}
// Load polyfills only when needed
loadPolyfillIfNeeded() {
return !window.IntersectionObserver
? import("intersection-observer")
: null;
}
// Feature flag controlled loading
loadAnalytics() {
return this.featureFlags().analytics
? import(/* "analytics" */ "./analytics/module")
: null;
}
}
Gotchas to watch out for
- Avoid barrel imports (index.ts files) for large modules
- Monitor bundle analyzer reports regularly
- Test tree-shaking effectiveness with production builds
- Be cautious with dynamic imports in server-side rendering
Performance Impact
Exceptional results:
- 40-70% reduction in final bundle size
- 50-80% improvement in tree-shaking effectiveness
- 30-50% faster parsing and evaluation time
- Significant improvement in mobile performance metrics
9. Use RxJS performance patterns
What it is
RxJS operators like shareReplay
, distinctUntilChanged
, and proper subscription management can significantly impact performance.
Why it matters
Inefficient observable patterns can cause memory leaks, unnecessary HTTP requests, and excessive change detection cycles.
How to apply
Step 1: Use shareReplay
for expensive operations:
@Injectable()
export class DataService {
private data$ = this.http
.get<Data[]>("/api/data")
.pipe(shareReplay({ bufferSize: 1, refCount: true }));
getData() {
return this.data$;
}
}
Step 2: Implement proper subscription management:
@Component({...})
export class MyComponent implements OnInit {
private destroyRef = inject(DestroyRef);
ngOnInit() {
this.dataService.getData()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(data => {
// Handle data
});
}
}
Step 3: Use an async pipe to avoid manual subscriptions:
@Component({
template: `
@if (user$ | async; as user) {
<div>
<p>{{ user.name }}</p>
<p>{{ user.email }}</p>
</div>
}
`,
})
export class UserComponent {
user$ = this.userService.getCurrentUser();
}
Gotchas to watch out for
- Avoid multiple async pipes on the same observable
- Use
distinctUntilChanged()
for frequently changing observables - Consider using
startWith()
for initial values
Performance Impact: Prevents memory leaks and reduces unnecessary network requests.
10. Implement virtual scrolling for large lists
What it is
Virtual scrolling renders only visible items in a list instead of creating DOM elements for thousands of items. This technique dramatically improves performance when displaying large datasets.
Why it matters
Rendering 10,000+ DOM elements can freeze browsers and consume excessive memory. Virtual scrolling maintains smooth performance regardless of data size by recycling DOM elements as users scroll.
How to apply
Step 1: Set up basic virtual scrolling:
npm install @angular/cdk
import { ScrollingModule } from "@angular/cdk/scrolling";
import { Component, signal } from "@angular/core";
@Component({
imports: [ScrollingModule],
template: `
<h3>Virtual List ({{ items().length }} items)</h3>
<cdk-virtual-scroll-viewport itemSize="50">
@for (item of items(); track item.id) {
<div>{{ item.name }}</div>
}
</cdk-virtual-scroll-viewport>
`,
})
export class VirtualListComponent {
items = signal(
Array.from({ length: 100000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
}))
);
}
Step 2: Optimize with buffer zones for smooth scrolling:
@Component({
template: `
<cdk-virtual-scroll-viewport
itemSize="60"
[maxBufferPx]="800"
[minBufferPx]="200"
>
@for (item of items(); track item.id) {
<div class="item">
<h4>{{ item.title }}</h4>
<p>{{ item.description }}</p>
</div>
}
</cdk-virtual-scroll-viewport>
`,
})
export class OptimizedVirtualListComponent {
items = signal<Item[]>([
/* your large dataset */
]);
}
Key configuration options
-
itemSize
: Fixed height of each item (required) -
maxBufferPx
: Extra pixels to render above/below viewport (default: 200px) -
minBufferPx
: Minimum buffer to maintain during scrolling (default: 100px)
Gotchas to watch out for
- Fixed heights work best: Dynamic sizing can cause performance issues
-
Buffer zones matter: Increase
maxBufferPx
for smoother scrolling on fast devices - Mobile testing: Test scrolling performance on actual devices
Performance impact
Outstanding results:
- Handles 100,000+ items smoothly
- Constant memory usage regardless of list size
- 90%+ reduction in initial render time
Performance checklist for Angular (2025)
Core foundations
- Enable zoneless change detection and remove Zone.js
- Prevent zone pollution with strategic NgZone usage
- Use signals with OnPush strategy for all components
- Implement strategic lazy loading with intelligent preloading
Bundle and rendering
- Optimize imports and avoid barrel imports
- Use @defer blocks and NgOptimizedImage
- Implement virtual scrolling for large datasets
Advanced patterns
- Configure SSR with PendingTasks API
- Use modern RxJS patterns and subscription management
Measuring success
Track these key metrics to validate your optimizations:
- Time to Interactive (TTI): Should improve by 50-70%
- Largest Contentful Paint (LCP): Target sub-2.5 seconds
- Cumulative Layout Shift (CLS): Aim for less than 0.1
- First Input Delay (FID): Should be under 100ms
Conclusion: Mastering Angular performance
In 2025, Angular performance optimization has evolved with powerful new tools like signals and zoneless architecture, giving developers more control and speed than ever. These ten techniques represent a significant leap forward from traditional optimization approaches, offering you the tools to build applications that feel truly exceptional.
Start with what matters most. Implementing zoneless change detection can improve your startup time by 60%. When you combine it with signals, you’ll notice how much smoother your application feels. Add strategic lazy loading, and you’ll see bundle sizes drop by 80%. Each technique works beautifully with the others.
You’re building the future of web applications. Your users will experience sub-second load times, instant interactions, and seamless navigation that feels as smooth as native applications. These aren’t just technical improvements but meaningful enhancements to user experience.
The path forward is clear. Angular has provided you with proven tools and techniques that deliver measurable results. Whether you implement them one by one or tackle several together, each step brings you closer to exceptional performance.
Consider starting with the techniques that align best with your current project needs. Your users will appreciate the difference, and you’ll gain confidence as you see the improvements in action.
The opportunity to create something remarkable is right in front of you.
If you have any questions, contact us through our support forum, support portal, or feedback portal. We are always happy to assist you!
Related Blogs
- Angular 19 Standalone Components: Build Faster, Simpler Apps Without NgModules
- Boost Angular Performance: Lazy Loading Guide
- Maximizing Angular Charts Performance with Lazy Loading
- Master Angular Signals in 2025: Build Faster, Smarter Angular Apps
This article was originally published at Syncfusion.com.
Top comments (0)