Angular Application Performance Optimization: Practical Techniques

Most slow Angular apps share the same causes: everything loaded up front, default change detection everywhere and leaky RxJS. The techniques that make the biggest difference, in order.

Angular applications have a reputation for being heavy. In my experience, that reputation comes less from the framework and more from a few recurring patterns: everything loaded up front, default change detection running across a huge component tree, and RxJS subscriptions that nobody cleans up.

These are the techniques that make the biggest practical difference, roughly in the order I apply them when auditing an Angular application.

1. Measure first

Before changing code, collect evidence:

  • Angular DevTools (browser extension) — the Profiler tab shows each change-detection cycle, which components were checked and how long they took.
  • Chrome Performance panel — reveals long tasks, forced layouts and whether time is spent in scripting or rendering.
  • Build stats — ng build --stats-json combined with a bundle analyser shows exactly what ends up in each bundle.
  • Lighthouse / Core Web Vitals — for load performance of public-facing pages.

Record a baseline: initial bundle size, time to interactive on a mid-range device, and the slowest user interactions.

2. Lazy load features

The single biggest load-time improvement in most enterprise Angular applications is to stop shipping the entire application on first load.

With standalone components, route-level lazy loading is straightforward:

export const routes: Routes = [
  { path: '', component: DashboardComponent },
  {
    path: 'reports',
    loadChildren: () => import('./reports/reports.routes').then((m) => m.REPORTS_ROUTES),
  },
  {
    path: 'settings',
    loadComponent: () => import('./settings/settings.component').then((m) => m.SettingsComponent),
  },
];

Go further with deferrable views (@defer) for heavy parts of a page that are not needed immediately — charts below the fold, rich editors, map widgets:

@defer (on viewport) {
  <app-sales-chart [data]="sales()" />
} @placeholder {
  <div class="chart-skeleton"></div>
}

Choose a preloading strategy deliberately. PreloadAllModules helps navigation speed but downloads everything eventually; a custom strategy that preloads only likely next routes is often a better balance.

3. Get change detection under control

By default, Angular checks every component in the tree whenever something might have changed — a click, a timer, an HTTP response. In a large tree with complex templates, that adds up.

Use OnPush

ChangeDetectionStrategy.OnPush tells Angular to check a component only when its inputs change by reference, when an event originates inside it, or when an observable bound with the async pipe (or a signal) emits.

@Component({
  selector: 'app-order-row',
  changeDetection: ChangeDetectionStrategy.OnPush,
  templateUrl: './order-row.component.html',
})
export class OrderRowComponent {
  order = input.required<Order>();
}

OnPush works best with immutable data: create a new object or array when data changes instead of mutating it. Mutation is the most common reason OnPush "breaks" an application — the data changed but the reference did not.

Adopt signals

Signals give Angular fine-grained knowledge of what changed. Components that read signals in their templates are updated precisely, and computed() values are memoised automatically. For new code, signal-based inputs and state are the direction the framework is heading, and they combine well with OnPush. Newer Angular versions can also run zoneless, removing Zone.js overhead entirely once the application is ready for it.

Avoid work in templates

Function calls in templates run on every change-detection cycle:

<!-- Runs on every check -->
<span>{{ calculateTotal(order) }}</span>

Use a computed() signal, a pure pipe, or precompute the value when data arrives.

Use track in loops

With the built-in control flow, always provide a stable identity:

@for (order of orders(); track order.id) {
  <app-order-row [order]="order" />
}

Without a meaningful track, Angular may destroy and recreate DOM for items that did not change.

4. Use RxJS carefully

RxJS is powerful, and most Angular performance and memory problems I debug involve it somewhere.

  • Clean up subscriptions. Prefer the async pipe or toSignal(), which handle unsubscription automatically. For manual subscriptions, use takeUntilDestroyed().
  • Pick the right flattening operator. switchMap for search (cancel stale requests), concatMap when order matters, exhaustMap for "ignore clicks while submitting". Using mergeMap everywhere leads to race conditions and duplicate requests.
  • Share expensive streams. Several async pipes on the same cold HTTP observable make several HTTP requests. Use shareReplay({ bufferSize: 1, refCount: true }) or convert to a signal once.
  • Debounce user input with debounceTime and distinctUntilChanged before hitting the API.
results = toSignal(
  this.query$.pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap((q) => this.api.search(q)),
  ),
  { initialValue: [] },
);

5. Reduce bundle size

  • Check budgets in angular.json and make the build fail when they are exceeded, rather than only warning.
  • Look for heavy dependencies pulled into the main bundle: full lodash, moment.js with all locales, entire icon sets, chart libraries on the landing screen.
  • Remove unused Angular Material or third-party modules, and import only the components you use.
  • Make sure production builds use the modern application builder (esbuild-based), which is significantly faster and produces smaller output than older webpack configurations.

6. Render large data efficiently

Tables with thousands of rows are common in enterprise applications. Use virtual scrolling (@angular/cdk/scrolling) so only visible rows exist in the DOM, or use server-side pagination. Combine this with OnPush row components and a proper track expression.

7. Consider server-side rendering where it matters

For public, SEO-relevant pages, Angular's SSR with hydration improves first paint and search visibility. For authenticated dashboards used on desktops, client rendering is usually fine, and SSR adds infrastructure without much benefit. Decide per application, not by default.

8. Architect components to stay fast

Performance is partly an architecture concern:

  • Keep presentational components simple, input-driven and OnPush.
  • Keep data loading in container components or services, not scattered across leaf components.
  • Avoid deeply nested component trees where each level adds bindings for data it only passes through.
  • Put shared state in services with signals or a well-structured store, not in ad-hoc component properties.

A realistic plan

For an existing application, I usually recommend this sequence:

  1. Baseline measurements and bundle analysis.
  2. Route-level lazy loading and removal of obviously heavy dependencies.
  3. OnPush and track on list-heavy and frequently updated components.
  4. RxJS clean-up: subscriptions, flattening operators, shared streams.
  5. Virtual scrolling or pagination for large data views.
  6. Gradual adoption of signals — and eventually zoneless — in new and refactored code.

Each step is measurable and can ship independently, so you get improvements without a risky rewrite. If your Angular application has become sluggish and you want a clear plan rather than guesswork, a focused performance audit is a good place to start.

  • Angular
  • Performance
  • RxJS
  • Change Detection
  • Signals

Need help with your project? Consult Ritesh Kumar.

Have a similar technical challenge?

Let's discuss your application, architecture or development requirements.

All articles

Performance 6 min read

Why Does My React Application Become Slow?

React apps become slow gradually. How to classify the slowness, measure it properly, and fix the seven causes I find most often in real projects.

Consulting 5 min read

When Should a Business Hire a Technical Consultant?

New product with no senior lead, an app that keeps getting harder to change, a bug nobody can fix — seven situations where outside technical expertise pays for itself, and a few where it does not.