DEV Community

Alejandro
Alejandro

Posted on

Part 1: Why Angular Applications Get Slower as They Grow

Part 1 of the Angular in Production series

Most Angular application don't become slow because someone madre one catastrophic mistake. Usually, the application starts normally. When you start, there are a few components, a few routes, some API calls, a couple of forms and everything feels fast enough. But then the application grows and more features are added, more dependencies are installed, more components start sharing data, more business logic moves into services and more API calls happen in the background. Eventually, someone notices that the application doesn't feel as responsive as it used to. The initial load takes longer, navigating between pages feels slower and some screens freeze briefly. A table that used to render instantly now takes noticeable time.

The first instinc is usually to look for one specific problem. Maybe Angular is running change detection too often or maybe the bundle is too large or there are too many subscription or a component needs OnPush. Sometimes that's correct but in larger applications, performance problems are usually the result of several small decisions accumulating over time.


Performance Problems Usually Accumulate

One of the things I've learned working with frontend applications is that performance problems rarely appear at the same time as the code that caused them. A component might be perfectly acceptable when it renders 20 items. But the same component might become a problem when it renders 10,000. A service might make one API request during a page load. After several features start depending on it, the same service might trigger five requests for the same data. A dependency might add 50KB to the application.

That doesn't matter much by itself but after several similar decisions, the initial JavaScript bundle becomes signifantly larger. The code didn't necessarily become bad overnight. The application simply grew beyond the assumptions that were made when the original code was written. This is why optimizing a large Angular application is often less about finding a single "magic fix" and more about understanding where the application is spending its time.


The Application Has More Than One Performance Problem

When someone says that the Angular app is slow, that can mean several completely different things. For example:

  • the first page takes too long to load
  • navigation feels slow
  • typing in a form is laggy
  • a table takes several seconds to render
  • API requests are duplicated
  • the browser uses too much memory
  • the application freezes during a particular interaction

These problems don't necessarily have the same cause. A slow initial load is usually a different problem from a slow table, and that is different from a memory leak and different from an API request that takes five seconds. I know this sounds obvious, but it's easy to forget when performance work starts with assumptions instead of measurements.


The First Thing I Check Is Usually Not Angular

When an application feels slow, my first instinct nowadays is not to immediately change the Angular code. I want to know where the time is going first.

This is why I don't like generic performance advice such as "Just use OnPush" or "Add lazy loading" or "Use signals". Those can all be useful but applying them without knowing the bottleneck is mostly guesswork.


A Large Bundle Is Not Always a Runtime Problem

One of the easiest performance problems to understand is the initial JavaScript bundle.
Imagine an application that downloads:

main.js           3.2 MB
vendor.js         1.8 MB
styles.css        400 KB
Enter fullscreen mode Exit fullscreen mode

The user needs to download, parse and execute all of that before the application becomes fully usable. That obviously create a bad initial experience. But reducing the bundle size won't necessarily fix a slow interaction after the application has loaded.
For example, this component might still be slow:

@Component({ 
    selector: 'app-orders',
    template:`
        <div *ngFor="let order of orders">
            {{ calculateTotal(order) }}
        </div>
    `
})
export class OrdersComponent{
    orders=[];
    calculateTotal(order: Order){
        return order.items.reduce(
            (total, item) => total + item.price * item.quantity,
            0
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

Even if the application has a tiny initial bundle, repeatedly doing expensive work during rendering can still make the interface feel slow. This is one of the reasons I try to separate two questions: how quickly can the application start and how efficiently does it behave once it is running. they are related but they are not the same problem.


Components Often Become the Place Where Everything Meets

Another common pattern appears as an application grows. A component starts small.

export class OrdersComponent {
    orders: Order[] = [];
}
Enter fullscreen mode Exit fullscreen mode

Then more features get added.

export classe OrdersComponent{
    orders: Order[] = [];

    filters= {};
    selected Order?: Order;

    isLoading= false;
    showModal= false;

    loadOrders() {}
    applyFilters() {}
    exportOrders() {}
    deleteOrder() {}
    calculateTotals() {}
    updatePagination() {}
}
Enter fullscreen mode Exit fullscreen mode

Eventually the component becomes responsible for:

  • loading data
  • transforming API responses
  • managing filters
  • handling forms
  • controlling modals
  • calculating business logic
  • managing pagination
  • displaying the UI

At that point, performance and maintainability problems often start appearing together. The component has too many responsibilities. A change in one area can affect anther and it becomes harder to understand what causes a render or which operation its expensive. And it becomes harder to optimize anything without worrying about breaking something else. I don't think the solution is to split every component into the smallest possible pieces that often creates a different kind of complexity.

The more question to ask yourself is if this component still have a clear responsibility and if the answer is no, performance work becomes much harder because the boundaries of the application are already unclear.


The Same Data Is Often Loaded More Than Once

One of the most common problems I see in growing applications is duplicated work.
For example:

ngOnInit() {
    this.userService.getCurrentUser()
        .subscribe(user => {
            this.user = user;
        });
}
Enter fullscreen mode Exit fullscreen mode

More than one component does this same thing and each component is independently asking for the same information. The application may end up making several requests for data that hasn't changed. This is not necessarily a problem when there are only two components but as the application grows, the number of requests can increase without anyone explicitly deciding that it should.
A shared data layer can help:

@Injectable({ 
    providedIn: 'root'
})
export class UserService  {
    private user$ = this.http
        .get<User>(`/api/user')
        .pipe(
           shareReplay({bufferSize: 1, refCount: true})
        );
    getCurrentUser(){
        return this.user$;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now multiple consumers can share the same request and cached result. The exact solution depends on the application. Sometimes a simple service is enough, others a query library is more appropiate and others the data should be loaded at the route level.

The important part is recognizing that every component independently loading the same data is often a sign that the application has no clear ownership of that data.


Performance Problems Are Often Architeture Problems

This is probably the main reason I don't like treating Angular performance as a collection of isolated tricks.
A slow application can be caused by:

  • too much JavaScript
  • expensive rendering
  • unnecessary change detection
  • duplicated API calls
  • large components
  • unbounded lists
  • memory leaks
  • unnecessary state synchronization

Those problems often have something in common. The application is doing work in places where it doesn't need to. Sometimes that means a component is recalculating something. Sometimes it means the same data is being fetched multiple times. Sometimes ut neabs the browser is loading code for a feature the user hasn't opened yet and others it means a subscription remains active long after the component that created it has disappeared. The solution is rarely to optimize everything. Usually, the biggest improvement comes from finding the work that matters most and removing or delaying it.


Start With the Biggest Bottleneck

When i start investigating a slow Angular application, I try not to optimize everything at once. That usually creates a lot of work without necessarily improving the user experience. Instead, I try to answer a simpler question: what is the application spending msot of its time doing?

For an initial load, that might be downloading and executing too much JavaScript. For a slow screen, it might be rendering thousands of DOM nodes. For a lagy interaction, it might be expensive work happening repeatedly during change detection. For a page that becomes slower over time, it might be a memory leak. The solution depends on the bottleneck.


Initial Load: Ship Less Code

If the problem is the initial bundle, the first things I usually look at are:

  • lazy-loaded routes
  • large third-party dependencies
  • code that is loaded before it is needed
  • duplicated libraries
  • unnecessarily large assets

For example, a feature that is only used by a small percentage of users probably shouldn't be part of the initial application bundle.

const routes: Routes = [
    {
        path: 'reports',
        loadChildren: () =>
            import('./reports/reports.routes')
                .then(m => m.REPORTS_ROUTES)
    }
];
Enter fullscreen mode Exit fullscreen mode

The user doesn't need to download the reporting feature before they have even opened it. The important point is that bundle optimization is only one part of the larger performance picture.


Rendering: Don't Make the Browser Do Unnecessary Work

Large collections are another common source of problems. Rendering thousands of elements at once can be expensive regardless of how well the rest of the application is structured.

<div *ngFor="let item of items">
    {{item.bane}}
</div>
Enter fullscreen mode Exit fullscreen mode

If items contains a few dozen elements, this is usually fine. If it contains thousands, the browser has to create and manage thousands of DOM nodes. At that point, pagination or virtual scrolling may be a better solution than trying to optimize the loop itself. The goal isn't always to make Angular render more efficiently. Sometimes the best optimization is simply not rendering things that the user cannot see.


Change Detection: Avoid Repeating Expensive Work

Another area I pay attention to is what happens during change detection. Code like this can become expensive:

<div>
    {{calculateTotal(order)}}
</div>
Enter fullscreen mode Exit fullscreen mode

If calculateTotal performs non-trivial work and is called repeatedly, the application may spend a surprising amount of time recalculating the same value. Moving the calculation to a more appropriate place can help:

orders= orders.map(order => ({
    ...order,
    total: calculateTotal(order)
}));
Enter fullscreen mode Exit fullscreen mode

Or, depending on the situation, using a more appropriate change detection strategy can reduce unnecessary work. But again, I wouldn't start by changing every component to OnPush without understanding the data flow. A performance optimization should solve a measured problem, not just follow a checklist.


Memory Problems Are Usually Les Obvious

Some performance probloems don't appear immediately. An application can feel perfectly fine when the user first opens it but after navigating through the application for several minutes, however, memory usage can continue increasing. One possible cause is a subscription that remains active after its component has been destroyed. Modern Angular provides tools that make cleanup easier:

import {takeUntilDestroyed} fronm '@angular/core/rxjs-interop'; 
constructor(){
    this.service.events$
        .pipe(takeUntilDestroyed())
        .subscribe(event => {
            // handle event
        });
}
Enter fullscreen mode Exit fullscreen mode

The specific solution depends on the situation, but the general problem is the same: A component should not continue doing work after it no longer exists. This is one of the reasons performance testing should include more than the initial page load.


Measure Before Changing the Architecture

The larger an Angular application becomes, the more tempting it is to start making large architectural changes.

Rewrite the state management. Replace the component structure. Migrate everything to a new pattern. Rewrite the API layer. Sometimes those changes are necessary but they can also create a lot of risk without addresing the actual bottleneck.

A more useful process is usually:

Measure --> Find the biggest bottleneck --> Change one thing --> Measure again --> Keep or revert the change
Enter fullscreen mode Exit fullscreen mode

This might mean checking:

  • bundle analysis
  • browser performace profiles
  • network requests
  • memory usage
  • rendering behavior

The important part is having evidence before and after the change.


Conclusion

Angular applications rarely become slow because of one bad decision. They become slow because the application keeps growing thile the assumptions behind the original architecture stay the same.

A component that was perfectly reasonable at the beggining may become too expensive once it renders thousands of items. A service that made one API call may eventually be used by ten different components. A dependency that seemed insignificant may become part of a large initial bundle. A subscription that seemed harmless may remain alive long after its component has disappeared.

This is why I don't think performance optimization should start with a list of Angular features to enable. The first step is understanding what the application is actually doing. Then the optimization becomes much more specific:

  • load less code
  • render less
  • avoid repeating expensive work
  • eliminate duplicated requests
  • clean up resources
  • improve the boundaries between parts of the application

The biggest improvements often come from removing unneessary work rather than making the existing work slightly faster.


Next Article in This Series

Part 2: Angular Bundle Size Is Only One Part of Performance




Thanks for reading! If your Angular application has accumulated performance or maintainability problems over time, this is the kind of work I help teams with: debugging existing codebases, improving performance and refactoring incrementally. You have a link to my upWork in my Dev profile.

Top comments (0)