Angular Performance: Targeted Updates and Less Upfront Work

This topic is part of Framework Performance. Angular has changed substantially in recent versions. Standalone components replaced NgModules as the default, signals introduced fine-grained reactivity, zoneless change detection removed the need for zone.js, @defer blocks made lazy loading of components declarative, and server-side rendering gained non-destructive hydration with event replay and incremental hydration. Applications built with these features can be fast by default; applications built on older patterns often carry avoidable costs.

The classic Angular performance problems come from change detection: with zone.js, every event, timer and HTTP response triggers a check of the whole component tree (unless components use OnPush), so large trees and frequent events cost main-thread time. Then there is bundle size — Angular apps ship a framework runtime plus application code, and eagerly loaded features grow the main bundle — and client-side rendering, where content waits for JavaScript. Modern Angular addresses each: signals and zoneless for targeted updates, @defer and lazy routes for smaller initial bundles, and SSR with hydration for fast first paint.

Zone.js change detection vs signals and zoneless Comparison of default zone.js change detection and signal-based zoneless updates in Angular. Zone.js change detection vs signals and zoneless zone.js (default CD) • Any async event triggers a check • Whole tree checked (unless OnPush) • zone.js patches browser APIs (bundle + overhead) Signals + zoneless • Updates scheduled when signals change • Only affected views refresh • No zone.js in the bundle

The Metric Degradation This Topic Addresses

Excessive change detection shows up as long tasks during interactions and timers, hurting INP — especially on pages with large component trees, frequent events (scroll, mousemove, websockets) or third-party scripts that trigger zone-patched callbacks. Large main bundles and client-side rendering hurt LCP and TBT. Destructive hydration in older Angular versions (re-rendering the DOM on bootstrap) caused flicker and CLS; modern non-destructive hydration avoids it.

Prerequisites

  • Angular DevTools (with the profiler) and the browser Performance panel.
  • Build stats (ng build --stats-json) and a bundle analyser such as source-map-explorer or esbuild's metafile analyser.
  • Field metrics per route.

1. Environment Setup: Profile Change Detection

Angular DevTools' profiler records change detection cycles, showing which components were checked and how long each took. Record a typical interaction and note how many cycles run and how long each takes.

2. Capture a Baseline

Record initial bundle size, LCP and INP per route in the field, and change detection time for key interactions in the lab with CPU throttling.

3. Isolate Causes

Frequent change detection cycles → zone.js triggering on events that do not change state (scroll handlers, polling, third-party timers). Long cycles → large trees without OnPush, expensive template expressions, functions called in templates. Large initial bundles → eagerly imported features, heavy libraries. Slow LCP → client-side rendering or late-loading hero content.

4. Apply Modern Angular Patterns

Adopt signals for component state and OnPush (or zoneless) change detection. Defer heavy, below-the-fold or interaction-only components with @defer. Lazy-load routes. Enable SSR with hydration and event replay. Use NgOptimizedImage for the LCP image.

typescript
// app.config.ts — zoneless change detection with SSR hydration.
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
export const appConfig = {
  providers: [
    provideExperimentalZonelessChangeDetection(),   // name varies by Angular version
    provideClientHydration(withEventReplay()),
  ],
};
// trade-off: zoneless requires state changes to notify Angular (signals, async pipe,
// markForCheck); code relying on zone.js to detect mutations must be updated first.

Change detection time per scroll-triggered cycle (dashboard) Bar chart comparing change detection time for a dashboard under default, OnPush and zoneless signal-based change detection. Change detection time per scroll-triggered cycle (dashboard) Default CD (zone.js) 38ms OnPush everywhere 9ms Signals + zoneless 2ms

Deconstructing Angular Change Detection

With zone.js, Angular wraps browser APIs (events, timers, promises, XHR) so that after any async callback, it runs change detection: it walks the component tree and re-evaluates template bindings, updating the DOM where values changed. Default components are checked on every cycle; OnPush components are checked only when their inputs change by reference, an event originates inside them, or they are explicitly marked. On large pages, default change detection on every scroll or timer event adds up.

Signals change the model. A signal is a reactive value; templates that read signals register dependencies, and when a signal changes, Angular knows exactly which views need refreshing. With zoneless change detection, there is no zone.js; Angular schedules change detection only when signals change, when markForCheck is called, or for events bound in templates. This makes updates targeted and removes zone.js patching overhead and its bundle size.

Advanced Diagnostics and Edge Cases

Functions in templates. {{ total() }} where total is a regular method re-runs on every check; use signals with computed or pure pipes.

Third-party libraries and zones. Libraries that use timers or event listeners trigger change detection; run them outside Angular (NgZone.runOutsideAngular) when using zone.js.

Large *ngFor/@for lists. Always use track with a stable identity; virtualize long lists with the CDK virtual scroll.

Hydration mismatches. Direct DOM manipulation or browser-only APIs during render break hydration; use afterNextRender for browser-only code.

Polyfills. Remove polyfills for browsers you no longer support; zone.js itself is removed with zoneless.

Validation and Budgeting

Use Angular's build budgets (angular.json budgets) to fail builds when initial bundles exceed limits. In the lab, compare change detection cycles and durations for key interactions. In the field, track LCP and INP per route.

BudgetTarget
Initial bundle (compressed)< 200 KB
Change detection per interaction (4x CPU)< 16 ms
Lazy routesAll non-landing features
SSR + hydrationEnabled for public routes

Worked Example: An Insurance Quote Application

An insurance company's quote application was a large Angular app with default change detection, client-side rendering and a 1.1MB (uncompressed) main bundle. LCP p75 on mobile was 4.0 seconds and INP p75 320ms; the quote form ran change detection on every keystroke across 900 components. The team enabled SSR with hydration for the landing and quote-start pages, moved the form to signals with OnPush components, deferred the comparison table and help chat with @defer, and lazy-loaded the account area. Initial bundle dropped to 420KB uncompressed, LCP p75 to 2.1 seconds, and INP p75 to 140ms. A later move to zoneless removed zone.js and another 30KB.

Bundle Size in Angular

The Angular CLI's esbuild-based builder produces efficient bundles, but application structure determines what is in the initial chunk. Every route not lazy-loaded, every component referenced from the root, and every library imported in eagerly loaded code ends up there. Use loadComponent and loadChildren for routes, @defer for components within pages, and check the build stats for large dependencies. Angular's build budgets give immediate feedback in CI when the initial bundle grows.

Server Rendering, Hydration and Incremental Hydration

Angular SSR renders pages on the server for fast first paint. Non-destructive hydration reuses the server DOM instead of re-creating it, avoiding flicker and CLS. Event replay records user interactions that happen before hydration completes and replays them afterwards, so early clicks are not lost. Incremental hydration combines @defer with hydration triggers (hydrate on viewport, hydrate on interaction), leaving parts of the server-rendered page dehydrated until needed — reducing the JavaScript that runs at load, similar to islands in other frameworks.

Angular SSR lifecycle with incremental hydration Steps from server rendering through hydration with event replay and incremental hydration of deferred blocks. Angular SSR lifecycle with incremental hydration Server render full HTML Paint fast LCP Hydrate core reuse DOM Replay events early clicks Hydrate blocks on viewport/interacti on

Images and Third-Party Scripts

NgOptimizedImage (the ngSrc directive) enforces dimensions, generates srcsets with image CDN loaders, and supports a priority attribute that preloads the LCP image with high fetch priority — and warns in development when the LCP image lacks it. Third-party scripts should load after the app is stable; when using zone.js, initialise them outside the Angular zone so their timers and listeners do not trigger change detection. With zoneless, this concern disappears, but scripts still compete for the main thread during load.

Template Performance Patterns

Many Angular slowdowns come from templates that do more work than they appear to. Method calls in bindings ({{ formatPrice(item) }}) run on every change detection cycle for every item; with default change detection and a busy page, that can mean thousands of calls per second. Replace them with pure pipes (cached per input) or computed signals. In @for loops, always provide a track expression with a stable identity, so Angular reuses DOM nodes when the list changes instead of recreating them. Avoid deeply nested structural directives that create and destroy large subtrees on toggles; @if with lightweight placeholders or [hidden] for frequently toggled content can be cheaper. For long lists, the CDK virtual scroll renders only visible rows.

Pipes deserve a note of their own. Pure pipes (the default) only re-run when their input reference changes, which makes them a good place for formatting and filtering. Impure pipes run on every cycle and should be avoided for anything expensive.

RxJS, Signals and Subscriptions

Angular apps often use RxJS for HTTP and event streams. Subscriptions that update component properties in subscribe callbacks rely on zone.js to refresh the view, and leaking subscriptions keep work running after components are destroyed. Prefer the async pipe or toSignal to bridge observables into templates — both handle subscription lifecycle and notify Angular of changes, which also prepares the code for zoneless. For high-frequency streams (websocket prices, mouse positions), throttle or sample them to the display rate, and update only the signals that drive visible values.

Measuring Angular Apps in the Field

Report Core Web Vitals per route with the web-vitals library, using the Angular router's active route configuration rather than raw URLs. Angular DevTools is invaluable in development, but field data decides priorities: it shows which routes have poor LCP (often client-rendered or image-heavy routes) and which interactions have poor INP (often large forms and dashboards). Long Animation Frames attribution helps separate framework work (change detection, rendering) from application logic and third-party scripts in slow interactions. Pair field data with lab traces of the same routes on a throttled profile, so each regression or improvement can be explained by a concrete change in the trace rather than guessed at.

Upgrading Older Angular Applications

Many Angular applications in production were built on older versions with NgModules, default change detection and client-side rendering. The upgrade path to modern performance features is incremental. First, keep Angular up to date — each major version improves the build pipeline and runtime, and the CLI's migrations automate much of the work (including converting to standalone components and the new control flow syntax). Next, adopt standalone components and the new @if/@for control flow, which are prerequisites for @defer. Then introduce signals in the components where interaction performance matters most, set them to OnPush, and add @defer blocks for heavy below-the-fold components. SSR with hydration and, finally, zoneless change detection complete the picture. Each step is valuable on its own, so teams can stop at any point with real gains. Measure after each step with the same throttled traces and field segments, so the team can see which changes moved LCP and INP and decide where to invest next.

A Rollout Plan

  1. Profile change detection and bundles; rank routes by traffic and metrics.
  2. Enable SSR with hydration and event replay for public routes.
  3. Lazy-load routes and defer heavy components with @defer.
  4. Convert hot components to signals and OnPush; remove functions from templates.
  5. Move to zoneless once state changes are signal-based or explicitly notified.
  6. Configure NgOptimizedImage with priority for the LCP image.
  7. Add build budgets and field monitoring per route.

Common Pitfalls

  • Default change detection on large trees. Every async event checks everything.
  • Methods called in templates. Recomputed on every cycle.
  • Eagerly loaded features. Large initial bundles.
  • Client-side rendering for public pages. Slow LCP.
  • Third-party scripts inside the zone. Trigger change detection constantly.

FAQ

What is zoneless change detection?

A mode where Angular runs without zone.js and schedules change detection only when signals change, events bound in templates fire, or code explicitly marks views for checking.

Should I use OnPush?

Yes, for most components. It limits checks to when inputs change or events occur inside the component, and prepares code for zoneless.

What are @defer blocks?

Template blocks that lazy-load a component's code and render it on a trigger (viewport, interaction, idle, timer), with placeholder and loading states.

Does Angular support SSR?

Yes. Angular SSR renders on the server, with non-destructive hydration, event replay and incremental hydration in recent versions.

Do signals replace RxJS?

Signals handle synchronous state and derived values; RxJS remains useful for complex async streams. Interop functions convert between them.

How do I find slow components?

Use the Angular DevTools profiler to record change detection and see time per component, then confirm with the browser Performance panel.

Is Angular slower than React or Vue?

Modern Angular with signals, zoneless and deferrable views performs comparably. Older patterns (default change detection, eager modules) are the usual cause of slowness.

Do standalone components improve performance?

They simplify lazy loading and @defer, because dependencies are declared per component, which makes it easier to keep code out of the initial bundle.

What is NgOptimizedImage?

A directive (ngSrc) that enforces image dimensions, generates responsive sources with image CDN loaders, and preloads the LCP image when marked with priority.

Should I use the CDK virtual scroll?

Yes, for long lists. It renders only visible items, keeping DOM size and change detection cost constant as lists grow.

How do I keep Angular bundles in check?

Set budgets in angular.json so builds warn or fail when bundles grow, and review the build stats when they do.

Do signals work with OnPush?

Yes. Signals read in an OnPush component’s template mark it for checking when they change, which is the recommended combination before going zoneless.

Is server-side rendering expensive for Angular apps?

Rendering costs server CPU for every uncached request. Prerender static routes and cache shared pages at the CDN so the server only renders what must be dynamic.

Can Angular apps use speculation rules?

Yes, for full page navigations between server-rendered or prerendered routes. Client-side navigations use the router’s preloading strategies instead.

Guides in This Topic

/html>