Angular performs well out of the box, but as an application grows, two things quietly slow it down: how often change detection runs, and how much JavaScript ships to the browser. Almost every optimisation below pulls one of those two levers. None of them require a rewrite - they are targeted changes you can apply incrementally and measure as you go.
Tame change detection
By default Angular checks the whole component tree after every event. On a large or data-heavy page that’s a lot of wasted work. The single most effective fix is OnPush change detection, which tells Angular to check a component only when one of its @Input references changes, an event originates inside it, or an observable it renders with the async pipe emits.
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
@Component({
selector: 'app-order-row',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<td>{{ order().reference }}</td>
<td>{{ order().total | currency }}</td>
`,
})
export class OrderRowComponent {
// A signal input: the row is only re-checked when this reference changes.
order = input.required<Order>();
}
Signals complement OnPush beautifully. Because a signal knows exactly which templates read it, Angular can update just the parts of the view that actually depend on the changed value instead of re-checking a whole component. Reaching for signals for UI state is one of the clearest paths to less change-detection work.
Keep templates cheap
Templates run on every check, so anything expensive in a binding is expensive repeatedly. Two habits matter most here:
- Never call methods in bindings for derived values. A
{{ getTotal() }}in the template re-runs on every change-detection cycle. Use a pure pipe or a computed signal instead - pure pipes only recompute when their inputs change. - Give
*ngForatrackBy. Without it, changing the array makes Angular tear down and rebuild every DOM node. AtrackBythat returns a stable id lets Angular reuse the existing nodes and only touch what changed.
Keep heavy computation out of the template entirely - do it in the component or a service and bind to the result.
Load less, and load it later
The fastest code is the code you never download until it’s needed. Angular gives you two complementary tools:
Lazy-loaded feature routes
Split your app by feature and load each route’s code on demand with loadComponent or loadChildren. The initial bundle only contains what the first screen needs; everything else arrives when the user navigates to it.
Deferrable views with @defer
Since Angular v17, the @defer block defers loading a section of a template - and its dependencies - until a trigger fires, such as the viewport being reached or the browser being idle. It’s ideal for below-the-fold widgets, charts and anything heavy that isn’t needed immediately.
@defer (on viewport) {
<app-analytics-chart [data]="metrics()" />
} @placeholder {
<div class="chart-skeleton"></div>
} @loading (after 100ms; minimum 300ms) {
<app-spinner />
}
Also prefer standalone components: they make dependencies explicit at the component level, which helps the compiler tree-shake and makes lazy loading a single component straightforward.
Long lists and images
Rendering thousands of rows at once is a common cause of jank. The Angular CDK’s virtual scrolling (cdk-virtual-scroll-viewport) renders only the items currently visible, so a list of ten thousand rows behaves like a list of twenty. For images, NgOptimizedImage (the ngSrc directive) enforces width and height to prevent layout shift, lazy-loads by default, and helps prioritise the largest above-the-fold image - a direct win for LCP and CLS.
For navigation, a route preloading strategy (such as PreloadAllModules or a custom one) fetches lazy chunks in the background after the initial load, so the next navigation feels instant without bloating the first paint.
Bundles, budgets and SSR
Production Angular builds already use AOT compilation and tree-shaking to strip unused code. Make that measurable: set bundle budgets in angular.json so a size regression fails the build rather than shipping silently, and analyse the output with source-map-explorer to see exactly which dependencies are large.
For perceived load time and SEO, use server-side rendering (SSR) with hydration. SSR sends fully rendered HTML so the first paint is fast and crawlable, then hydration attaches interactivity without re-rendering everything from scratch. If you’d like this set up end to end, our Angular development service handles SSR, budgets and the full performance pipeline.
Common mistakes to avoid
- Calling functions in templates for derived data. They re-run on every cycle; use pure pipes or computed signals.
- Forgetting
trackByon large*ngForlists. It forces needless DOM churn on every update. - Leaving change detection on Default everywhere. Data-heavy components benefit hugely from OnPush.
- Not unsubscribing from long-lived observables. Prefer the
asyncpipe ortakeUntilDestroyedto avoid leaks that quietly degrade performance. - Shipping one giant bundle. No lazy routes or
@defermeans users download code for screens they may never visit.
Best practices checklist
- Default to OnPush and use Signals for UI state.
- Lazy-load feature routes and
@deferheavy or below-the-fold blocks. - Add
trackByand pure pipes; keep template bindings trivial. - Use CDK virtual scrolling for long lists and
NgOptimizedImagefor images. - Set bundle budgets, analyse with source-map-explorer, and enable SSR with hydration.



