Skip to content
Subscribe to RSS Find me on GitHub Follow me on Twitter

How to Optimize Performance in Angular Applications

How to Optimize Performance in Angular Applications

Building Angular apps that feel fast and responsive is one of the most satisfying parts of web development. You click a button and something happens instantly. You scroll through a long list and it doesn’t stutter. But as your application grows, you might notice it starts to feel sluggish. Buttons take a moment to respond, lists flicker when you update them, and the initial load time creeps up. These are performance issues, and they’re completely normal as your app gets more complex.

The good news? Angular gives you powerful tools to keep your app running smoothly. You don’t need to be an expert to make a big difference. This guide will walk you through three simple techniques that can dramatically improve your app’s speed. We’ll focus on two core ideas: reducing unnecessary work and loading only what you need, when you need it.

Understanding the Performance Challenge

Before we dive into solutions, let’s understand what makes Angular apps slow in the first place. Angular’s magic comes from its ability to automatically update the screen when your data changes. This is called change detection. When you click a button, fetch data, or type in a form, Angular checks every part of your app to see if anything on the screen needs updating.

Imagine you have a to-do list app with 100 tasks. When you mark one task as complete, Angular’s default behavior is to check all 100 tasks to see if anything changed. That’s like searching your entire house for your keys when you know they’re in your pocket. It’s reliable, but it’s overkill.

For small apps, this isn’t a problem. But as you add more components, more lists, and more complex features, these checks add up. Your app spends more time checking for changes than actually making them. The techniques in this guide help Angular work smarter, not harder.

Step-by-Step Optimization Guide

Let’s walk through three practical steps you can implement today. Each one builds on the last, and you can use them independently or together.

Step 1: Use OnPush Change Detection

The easiest win is to tell Angular: “Only check this component when its inputs change.” This is called the OnPush change detection strategy. By default, Angular uses the Default strategy, which checks components on every single change detection cycle. Switching to OnPush can cut your change detection work by 80% or more.

Here’s how to implement it:

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

@Component({
  selector: 'app-user-card',
  templateUrl: './user-card.component.html',
  styleUrls: ['./user-card.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush  // Add this line
})
export class UserCardComponent {
  @Input() user: { name: string; email: string } | null = null;
  
  get initials(): string {
    if (!this.user?.name) return '';
    return this.user.name.split(' ').map(n => n[0]).join('').toUpperCase();
  }
}

What this code does:

Plain language explanation: This tells Angular, “Hey, only bother checking this component for updates if the user input actually changes.” If the parent component updates other data, this UserCardComponent gets skipped entirely. It’s like telling your brain to only think about your keys when you actually hear them jingle, not every time you walk through a doorway.

When to use it: Apply OnPush to any component that only depends on its @Input() properties. Presentation components like cards, lists, and forms are perfect candidates. Be more careful with components that manage a lot of internal state or use services extensively.

Step 2: Add TrackBy for Lists

Have you ever noticed a list flickering or completely re-rendering when you add a single item? That’s because Angular doesn’t know which items are new, moved, or unchanged. By default, when you use *ngFor to loop through an array, Angular tracks items by their object references. If the array reference changes, Angular destroys and recreates every DOM element in the list. This is slow and creates a janky user experience.

trackBy solves this by giving Angular a unique identifier for each item. Instead of rebuilding the entire list, Angular only adds, removes, or moves the items that actually changed.

Here’s a complete example:

// In your component TypeScript file
import { Component } from '@angular/core';

@Component({
  selector: 'app-task-list',
  templateUrl: './task-list.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TaskListComponent {
  tasks = [
    { id: '1', title: 'Buy groceries', completed: false },
    { id: '2', title: 'Walk the dog', completed: true },
    { id: '3', title: 'Finish report', completed: false }
  ];

  // This function tells Angular how to identify each task
  trackByTaskId(index: number, task: { id: string; title: string; completed: boolean }): string {
    return task.id;  // Return the unique identifier
  }

  addTask() {
    const newTask = {
      id: Date.now().toString(),  // Simple unique ID
      title: 'New task',
      completed: false
    };
    this.tasks = [...this.tasks, newTask];  // Create new array reference
  }
}
<!-- In your component template -->
<div class="task-list">
  <button (click)="addTask()">Add Task</button>
  
  <ul>
    <li *ngFor="let task of tasks; trackBy: trackByTaskId">
      <input type="checkbox" [checked]="task.completed" />
      {{ task.title }}
    </li>
  </ul>
</div>

What this code does:

Plain language explanation: Think of trackBy like giving each list item a name tag at a party. Without name tags, if someone new arrives, you’d have to ask everyone’s name again. With name tags, you just greet the new person and move on. Angular does the same thing—it remembers each item by its ID and only deals with the ones that actually changed.

Pro tip: Always use trackBy for lists that can change, especially long lists. The performance difference is dramatic with 50+ items.

Step 3: Lazy-Load Feature Modules

The first two techniques made Angular’s runtime faster. Now let’s make your app load faster. When users first visit your app, they download the entire application—even pages they might never visit. Lazy loading changes this by splitting your app into smaller chunks and only loading them when needed.

Imagine you have an admin dashboard that only 5% of users ever see. Why make everyone download that code? With lazy loading, the admin module sits on the server until someone actually navigates to /admin. Then Angular fetches it in the background.

Here’s how to set it up:

// In your app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';

const routes: Routes = [
  {
    path: '',
    component: HomeComponent
  },
  {
    path: 'admin',
    // This line tells Angular to load the admin module only when needed
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
  },
  {
    path: 'reports',
    loadChildren: () => import('./reports/reports.module').then(m => m.ReportsModule)
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }
// In your admin/admin-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AdminDashboardComponent } from './admin-dashboard.component';

const routes: Routes = [
  {
    path: '',
    component: AdminDashboardComponent
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class AdminRoutingModule { }

What this code does:

Plain language explanation: This is like ordering food at a restaurant. Instead of bringing every dish to your table when you sit down (eager loading), the kitchen waits for you to order each dish (lazy loading). Your initial “bundle” is much smaller, so your app loads faster. When you click a link to the admin area, Angular says “Oh, I need that module!” and fetches it just in time.

How to verify it’s working: Open your browser’s DevTools, go to the Network tab, and look for JavaScript files being loaded when you navigate to different routes. You should see new chunks appear when you first visit /admin or /reports.

Common Mistakes

Even with the best techniques, it’s easy to fall into these traps. Here’s what to watch out for:

1. Forgetting to update references with OnPush: When you use OnPush, Angular only checks inputs that have new object references. If you mutate an object instead of creating a new one, Angular won’t detect the change.

// ❌ Wrong - mutates existing object
this.user.name = 'New Name';  // OnPush component won't update

// ✅ Correct - creates new object
this.user = { ...this.user, name: 'New Name' };  // OnPush component updates

2. Using index as trackBy: It’s tempting to use the array index as a unique identifier, but this defeats the purpose. If items reorder, Angular will get confused.

// ❌ Wrong - index changes when items move
trackByIndex(index: number): number {
  return index;
}

// ✅ Correct - use a stable ID
trackByItemId(index: number, item: any): string {
  return item.id;
}

3. Over-optimizing too early: Don’t apply these techniques everywhere from day one. Start with the components that cause actual performance problems. Premature optimization makes your code harder to maintain without real benefits.

4. Not measuring first: You can’t improve what you don’t measure. Always profile your app before and after making changes. Sometimes the bottleneck is in an unexpected place.

5. Forgetting to test after lazy loading: When you split modules, you might accidentally create multiple instances of services. Make sure services that should be singletons are provided in the root module, not in lazy-loaded modules.

Troubleshooting

When things don’t work as expected, here’s how to get back on track:

Problem: My OnPush component doesn’t update when data changes

Problem: List still flickers after adding trackBy

Problem: Lazy-loaded routes throw “Module not found” errors

Problem: Initial load is still slow after lazy loading

Problem: Everything feels slow but I don’t know why

Practice Exercises

The best way to learn is by doing. Try these exercises to solidify your understanding:

Exercise 1: Build a Performance-Optimized Contact List

  1. Create a new Angular component called ContactListComponent
  2. Display a list of 100 contacts with names and emails
  3. Implement OnPush change detection
  4. Add a trackBy function using contact IDs
  5. Add a button that randomly updates one contact’s email
  6. Verify that only that one list item re-renders (use Angular DevTools to confirm)

Exercise 2: Lazy-Load a Settings Page

  1. Generate a new feature module: ng generate module settings --routing
  2. Create a SettingsComponent inside this module
  3. Add a route to lazy-load this module at /settings
  4. Build your app and check the output—confirm you see a separate chunk for the settings module
  5. Open the Network tab and verify the settings chunk only loads when you navigate to /settings

Exercise 3: Create a Reusable TrackBy Factory

  1. Write a function that generates trackBy functions for any property name:
export function createTrackBy<T>(property: keyof T): (index: number, item: T) => any {
  return (index: number, item: T) => item[property];
}
  1. Use it in a component: trackById = createTrackBy<Task>('id');
  2. Apply this pattern to three different lists in your app
  3. Notice how this reduces code duplication while keeping type safety

Exercise 4: Performance Toggle Switch

  1. Build a component that displays a large dataset
  2. Add a button that toggles between Default and OnPush strategies
  3. Measure the change detection time in both modes using ng.profiler.timeChangeDetection()
  4. Document the performance difference in a comment
  5. This helps you understand the real impact of your optimizations

Next Steps

You’ve learned three powerful techniques, but performance optimization is a journey. Here’s where to go from here:

Dive deeper into change detection: Learn about ChangeDetectorRef and how to manually trigger change detection when you need to. This gives you fine-grained control in complex scenarios.

Explore advanced bundling: Look into Angular’s esbuild builder and how to configure custom webpack builds for even more aggressive code splitting. The ngx-build-plus package lets you customize the build process.

Master state management: As apps grow, consider using NgRx or NgXs. These libraries are designed to work seamlessly with OnPush and make state changes more predictable and easier to optimize.

Set up performance budgets: In your angular.json, configure budgets to fail builds if your bundles exceed certain sizes. This catches performance regressions before they reach production.

"budgets": [
  {
    "type": "initial",
    "maximumWarning": "500kb",
    "maximumError": "1mb"
  }
]

Monitor production performance: Use tools like Google Lighthouse, Web Vitals, or Sentry to track real user performance. Lab tests are useful, but real-world data tells the full story.

Join the community: Follow Angular’s official blog and the #angular-performance hashtag on Twitter. The community regularly shares new techniques and best practices.

Summary

Optimizing Angular performance doesn’t require rewriting your entire app. By focusing on three core techniques—OnPush change detection, trackBy for lists, and lazy loading modules—you can make dramatic improvements with minimal code changes.

Remember the key principles:

Start small. Pick one component that’s causing performance issues and apply OnPush. Measure the improvement. Then move to your biggest list and add trackBy. Finally, identify a feature that most users don’t need and lazy-load it. Each step will make your app feel snappier and more professional.

Performance optimization is a skill that grows with practice. The exercises in this guide give you a safe space to experiment. The troubleshooting section arms you with tools to diagnose issues. And the next steps point you toward deeper mastery.

Your users will notice the difference. And you’ll have the satisfaction of knowing your Angular app isn’t just feature-rich—it’s fast, efficient, and a joy to use.