Skip to content
Angular ng performance 4 min read

Runtime Performance Profiling

When an Angular app feels sluggish, the cause is almost always one of two things: change detection running too often, or individual components doing too much work per cycle. Guessing at the culprit wastes time. Profiling replaces guesswork with measured data — letting you see exactly which components are re-rendering, how long each change detection pass takes, and what triggered it. This page walks through Angular DevTools and the browser’s own profiler to find and fix runtime hotspots.

Installing Angular DevTools

Angular DevTools is a browser extension for Chrome and Firefox. It connects to apps built in development mode and exposes a component tree inspector and a change detection profiler. Install it from the Chrome Web Store or Firefox Add-ons, then open your app and look for the Angular tab in the browser DevTools panel.

The extension only attaches when Angular is running in development mode. If you serve a production build (ng build without --configuration development), the global debug APIs are stripped and the tab will report that it cannot connect.

ng serve

The extension needs enableProdMode() to be off. Production builds intentionally disable the profiling hooks for performance, so always profile against a dev server or a debug build.

Reading the component tree

The Components tab mirrors your live component hierarchy. Selecting a node shows its current @Input() values, signal values, and injected dependencies. You can edit inputs inline to test how a component reacts without touching code.

More importantly, the tree visualizes change detection strategy. Components using ChangeDetectionStrategy.OnPush are marked, so you can confirm at a glance that your optimization actually took effect.

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';

@Component({
  selector: 'app-cart',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @for (item of items(); track item.id) {
      <li>{{ item.name }} — {{ item.price | currency }}</li>
    }
  `,
})
export class CartComponent {
  readonly items = signal<{ id: number; name: string; price: number }[]>([]);
}

Profiling change detection

The Profiler tab is the core tool. Click the record button, interact with your app (click, type, scroll), then stop recording. DevTools renders a bar chart where each bar is one change detection cycle, and the height represents how long that cycle took.

Click any bar to drill into a flame-graph-style breakdown showing which components were checked and how many milliseconds each consumed. A component highlighted in a hot color is doing expensive work — often a heavy getter, an unmemoized pipe, or a template binding that calls a function on every cycle.

The profiler also tells you why a cycle ran. Hovering a frame surfaces the trigger source, such as an event handler, a timer, an HTTP response, or a signal update. This is the fastest way to discover that a setInterval or a third-party library is forcing detection far more often than intended.

Profiler signalLikely causeFix
Many tiny cyclesFrequent events (mousemove, scroll)Debounce, run outside Angular zone
One huge cycleLarge list re-rendered fullyAdd track, use OnPush, virtual scroll
Component checked but unchangedDefault change detectionSwitch to OnPush + signals
Function call in flame graphMethod binding in templateMove to a computed() signal

Moving work out of change detection

A common discovery is that a high-frequency DOM event is triggering hundreds of change detection cycles. Use NgZone.runOutsideAngular() to run that work without scheduling detection, then re-enter the zone only when state genuinely changes.

import { Component, NgZone, inject, signal } from '@angular/core';

@Component({
  selector: 'app-tracker',
  standalone: true,
  template: `<p>Pointer X: {{ x() }}</p>`,
})
export class TrackerComponent {
  private readonly zone = inject(NgZone);
  readonly x = signal(0);

  ngOnInit(): void {
    this.zone.runOutsideAngular(() => {
      window.addEventListener('pointermove', (e) => {
        // No change detection here — cheap.
        if (Math.abs(e.clientX - this.x()) > 50) {
          this.zone.run(() => this.x.set(e.clientX));
        }
      });
    });
  }
}

Using the browser performance profiler

Angular DevTools shows framework-level cost. For deeper analysis — layout, paint, scripting, garbage collection — use the browser’s Performance tab. Record a session, then inspect the flame chart for long tasks.

Angular emits named markers you can filter for in the timeline, such as NgZone work and component lifecycle hooks. Combine this with the Performance insights panel to catch layout thrashing or forced reflows that Angular DevTools alone will not reveal.

Output:

Total Blocking Time: 420 ms
Longest task: 180 ms  (change detection: ProductGridComponent)
Forced reflows: 12     (style read after write)

A long task over 50 ms blocks the main thread and hurts interactivity. Correlate the timestamp of a long task in the Performance tab with the matching cycle in the Angular profiler to pinpoint the responsible component.

Best Practices

  • Always profile against a development build; production builds strip the hooks DevTools relies on.
  • Record a realistic interaction, not an idle page — profile the actual slow flow your users report.
  • Look for repeated small cycles first; high-frequency triggers usually cause more pain than one big render.
  • Confirm OnPush and signal adoption in the component tree rather than assuming the change applied.
  • Cross-reference Angular DevTools cycles with browser Performance long tasks for layout and paint costs.
  • Re-profile after each fix so you measure the impact instead of guessing whether it helped.
  • Keep runOutsideAngular() boundaries narrow — re-enter the zone the moment real state changes.
Last updated June 14, 2026
Was this helpful?