2부: 각도 번들 크기는 퍼포먼스의 한 부분일 뿐입니다

작성자

카테고리:

← 피드로
DEV Community · Alejandro · 2026-07-20 개발(SW)

Part 2 of the Angular in Production series
One of the first things people usually check when an Angular application feels slow is the bundle size. That makes sense. If the browser has to download several megabytes of JavaScript before the application can become interactive, the initial experience will probably suffer.

So developers start looking at:

  • bundle analyzers
  • lazy-loaded routes
  • tree shaking
  • third-party dependencies
  • unused code
  • build configuration

These are all important, but reducing the JavaScript bundle is only one part of improving performance. A smaller bundle can make an application load faster. It does not automatically make the application fast once it is running. That distinction is easy to miss.

Loading Performance and Runtime Performance Are Different Problems

Imagine an Angular application with a 3 MB initial JavaScript bundle. The application takes several seconds to become interactive. After optimizing the bundle, the initial JavaScript is reduced to 1 MB.

The first load improves significantly. But after the application starts, opening a large table still freezes the browser for two seconds. The bundle optimization worked, it just didn’t solve the second problem. The application had two different performance bottlenecks. One was related to loading and the other was related to runtime work.

I try to think about performance in separate stages:

User opens the application --> Browser downloads resources --> Browser parses and executes JavaScript --> Angular bootstraps --> Application renders --> User interacts with the application

Enter fullscreen mode Exit fullscreen mode

Each stage can have a different bottleneck. Optimizing on stage does not necessarily improve the others.

A smaller Bundle Still Has to Be Executed

Reducing bundle size is valuable, but the number of bytes downloaded is not the only thing that matters.

The browser also has to:

  • download the JavaScript
  • parse it
  • compile it
  • execute it
  • create the initial application
  • render the result

This means that two bundles with similar sizes can still behave differently. For example:

Application A
1.5 MB JavaScript
Simple initialization
Minimal startup work

Enter fullscreen mode Exit fullscreen mode

And:

Application B
1.5 MB JavaScript
Heavy initialization
Multiple API requests
Large amount of synchronous work

Enter fullscreen mode Exit fullscreen mode

They have a similar bundle size. The user experience can still be very different. This is why I try not to use bundle size as a single definition of frontend performance. It is an important metric. It is just not the entire picture.

The Initial Bundle Should contain What the User Needs First

One of the most efffetive ways to improve the initial experience is to avoid loading code before it is needed. Consider an application with routes like:

/dashboard
/orders
/reports
/admin
/settings

Enter fullscreen mode Exit fullscreen mode

A user visiting /dashboard probably doesn’t need the entire reporting system and administration panel immediately. Loading all of those features upfront means the browser has to download code for functionality the user may never use.

Lazy loading gives the application a better boundary:

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

Enter fullscreen mode Exit fullscreen mode

Now the reporting code can be loaded when the user actually navigates to that part of the application.
The idea is simple: The initial bundle should contain what is needed to start the application, not necessarily everything the application can do.
This becomes increasingly important as an application grows.

Dependencies Can Quietly Become a Performance Problem

One of the easiest ways to increase bundle size is to add another dependency. At the time, the decision often looks completely reasonable.

You need a date utility --> you install a library
You need a chart --> you install another library
You need a component --> you install another package

Enter fullscreen mode Exit fullscreen mode


plaintext
None of these decisions seems particulary important in isolation. Eventually, however, the application contains a large collection of dependencies. Some of them might be used everywhere. Others might only be used by one feature and others might only be used by one feature. Some might have been added years ago and barely used anymore.
For example:

import * as utils from 'large-utility-library';

Enter fullscreen mode Exit fullscreen mode


plaintext
may bring a very different amount of code into the application than importing only what is actually needed:

import {formatDate} from 'large-utility-library/date';

Enter fullscreen mode Exit fullscreen mode


plaintext
The exact behaviour depends on the package and the build tooling, so I wouldn’t assume that every import style automatically produces a problem. But I do think dependencies deserve periodic review. A package is not free just because installing it takes one command. I can affect:

  • bundle size
  • build time
  • startup time
  • maintenance
  • security updates
  • future migrations

Third-Party Code Is Still Part of Your Performace Budget

A common mistaje is to think only about the code written inside the application. But the browser doesn’t care who wrote the code. Your code and third-party code are still part of the same application. A library might provide useful functionality while also adding significant JavaScript to the initial load.

For example:

Your application + UI library + Chart library + Editor + Analytics + Date library --> Initial JavaScript

Enter fullscreen mode Exit fullscreen mode


plaintext
Sometimes the library is absolutely worth the cost. A complex feature can be much cheaper to maintain by using an established library instead of implementing everything yourself. The question isn’t if you can remove every dependency. That would usually be unrealistic. The better question is if this dependency need to be part of the initial experience.

A rich text editor used only on an admin page probably shouldn’t necessarily be downloaded by every user visiting the public application. A charting library used only on one route might be a good candidate for lazy loading. The optimization is not always removing the dependency. Sometimes it is moving it to the point where it is actually needed.

Lazy Loading Can Also Be Applied to Features

Route-level lazy loading is usually the first place people look. But the same principle can apply inside a feature. For example, imagine a dashboard with several expensive sections:

Dashboard
├-- Summary
├-- Activity
├-- Analytics
└-- Export tools

Enter fullscreen mode Exit fullscreen mode


plaintext
The user may only need the summary immediately. Loading evert chart, export utility and analytics module before the user interacts with them can increase the cost of the initial page. In come cases, loading heavy functionality on demand makes more sense.
The implementation depends on the feature and the Angular version, but the architectural idea is the same:

Load the application --> Load the current feature --> Load expensive functionality --> Only when needed

Enter fullscreen mode Exit fullscreen mode


plaintext
This is especially useful for features such as:

  • data visualization
  • rich text editors
  • PDF generation
  • large admin tools
  • complex configuration interfaces

The user shouldn’t necessarily pay the performance cost of functionality they ever use.

But Lazy Loading Is Not Automatically Better

There is also a point where lazy loading can become excessive. If an application splits every small component into a separate chunk, the result may become difficult to reason about. The browser may need to make many additional requests. The application may spend mor time coordinating chunks than it would have spent loading a slightly larger initial bundle.

This is why I don’t think the goal should be to make the initial bundle as small as possible. But the better goal is making the initial bundle appropriate for the first experience. A small bundle that requires many additional requests before the user can actually use the application may not be better than a slightly larger bundle that starts immediately. Performance is about the entire experience, not a single number from a build report.

The Next Bottleneck Is Often JavaScript Execution

Even after reducing the amount of JavaScript downloaded, the application can still feel slow. At this point the problem may be the amount of work the browser has to perform after the code arrives. For example:

@Component({ 
    template: `
        <div *ngFor="let product of products">
            {{ calculatePrice(product) }}
        </div>
    `
})
export class ProductListComponent {
    calculatePrice(product: Product) {
        return product.price*product.quantity;
    }
}

Enter fullscreen mode Exit fullscreen mode


plaintext
For small list, this may not matter, but for a large list, repeated calculation during rendering can become expensive. The problem here isn’t the size of the bundle but the amount of work being performed during runtime. This is where bundle optimization ends and application performance begins.

Runtime Performance Starts After the Bundle Has Loaded

When people talk about frontend performance, i’ts easy to focus on everything that happens before the application becomes interactive. But once the JavaScript has been downloaded and Angular has bootstrapped the application, a completely different set of problems can appear.

This is where the application spends most of its lifetime. The user navigates between routes and tables update, forms change, charts render, dialogs open and close, search result are filtered and if those interactions feel slow, reducing the bundle size won’t help anymore. The problem is no longer how much code the browser downloaded. The problem is how much work the application is doing while it runs.

Rendering the Same Work Over and Over

One of the most common sources of runtime performance issues is repeated work. Take this simple component:

@Component({ 
    selector: 'app-products',
    template: `
        <div *ngFor="let product of products">
            {{ calculatePrice(product) }}
        </div>
    `
})
export class ProductsComponent {
    products: Product[] = [];
    calculatePrice(product: Product){
        return product.price*product.quantity;
    }
}

Enter fullscreen mode Exit fullscreen mode


typescript
There’s nothing technically wrong but if the list contains several thousand products and Angular evaluates that function repeatedly during change detection, the cost starts to add up. The calculation itself isn’t expensive. Repeating it hundreds or thousands of times is. In cases like this, I usually try to move the work closer to where the data is created instead of where it’s rendered.

products = apiProducts.map(product => ({
    ...product,
    totalPrice: product.price*product.quantity
}));

Enter fullscreen mode Exit fullscreen mode


typescript
Now the template becomes much simpler:

<div *ngFor="let product of products">
    {{ product.totalPrice }}
</div>

Enter fullscreen mode Exit fullscreen mode


plaintext
Now, the browser spends less time recalculating values that haven’t changed.

A Fast Route Can Still Feel Slow

Sometimes the route itself loads quickly. The API responds almost instantly and the bundle is already cached. Navigation completes in a fraction of a second. And yet the page still feels sluggish. This usually means the bottleneck isn’t the network anymore. It’s rendering. For example, rendering a large table:

<tr *ngFor="let order of orders">
    ...
</tr>

Enter fullscreen mode Exit fullscreen mode


typescript
If orders contains ten thousand elements, Angular isn’t necessarily the problem. The browser now has to create, paint and maintain ten thousand DOM nodes. No framework can completely avoid that cost.

This is why techniques like pagination or virtual scrolling often have a much larger impact than trying to optimize change detection. The fastest DOM node is the one that doesn’t exist.

Change Detection Shouldn’t Become the Scapegoat

Angular’s change detection often gets blamed whenever an application becomes slow. Sometimes that’s fair. Most of the time, though, it’s simply exposing expensive code that already exists. Consider this example:

<div *ngFor="let user of users">
    {{ fullName(user) }}
</div>

Enter fullscreen mode Exit fullscreen mode


typescript
The problem isn’t that Angular is caling fullName(). The real question is why does rendering this component require recalculating the same value over and over.
Changing the component to OnPush may reduce how often Angular checks it. It doesn’t remove unnecessary work.

Whenever I profile an application, I try to optimize the work itself before changing the detection strategy. If the application performs less work, every change detection cycle neturally becomes cheaper.

Network Performance Is Also Part of Frontend Performance

Another thing that’s easy to overlook is duplicated network requests. I’ve seen applications where several components independently request exactly the same data.

this.http.get('/api/profile');

Enter fullscreen mode Exit fullscreen mode

Then another component that is the same and another and another. Each request may be relatively fast. Together they create unnecessary network traffic, duplicated parsing and additional rendering work.

Caching or sharing data often provides a larger performance improvement than shaving another fifty kilobytes off the JavaScript bundle. Performance isn’t just about the browser. It’s also about avoiding unnecessary work across the entire application.

My Personal Rule

One idea I come back to repeatedly is that performance works like a budget. Every decision consumes a small part of it. That means another dependency, animation, HTTP request, another large component, subscription and another expensive calcutation. And none of these choices seems particularly important on its own. But after hundreds of similar decisions, the application starts feeling heavier. That’s why I rarely ask if the optimization is worth it and i ask instead if the additional work makes worth making every user pay for it. Sometimes the answer is absolutely yes and others it’s clear that the work could happen later or not at all.

Conclusion

Bundle size is one of the easiest performance to measure. It’s visible in every build, easy to compare between commits and satisfying to reduce but it’s only the beginning. Users don’t experience performance as a bundle size. That experience depends on much more than the amount of JavaScript downloaded. It depeds on:

  • how much work happens during rendering
  • how often expensive calculations are repeated
  • how many unnecessary network requests are made
  • how much DOM the browser has to manage
  • how much code is loaded before it’s actually needed

Optimizing Angular applications isn’t about chasing a single number. It’s about reducing unnecessary work throughout the application’s lifetime.

Sometimes that means making the bundle smaller, others it means rendering less and others moving calculations. Sometimes it simply means asking whether the application is doing work that nobody benefits form.

Next Article in This Series

Part 3: The Angular Change Detection Mistakes That Make Large Apps Feel Slow

Previous Article in This Series

Part 1: Why Angular Applications Get Slower as They Grow

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.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다