Skip to main content

Angular Bundling

Introduction

When you build an Angular application, your code, templates, styles, and dependencies need to be transformed and packaged in a way that browsers can understand and load efficiently. This process is called bundling. Effective bundling is essential for Angular application performance as it directly impacts loading times, execution speed, and resource utilization.

In this guide, we'll explore what bundling is in the context of Angular applications, why it matters, and how you can optimize your bundle structure to create high-performing applications.

What is Bundling?

Bundling is the process of combining multiple files into a single file (a bundle) that can be served to the browser. At a high level, bundling:

  1. Combines many small JavaScript files into fewer, larger files
  2. Applies optimizations like minification and tree-shaking
  3. Organizes code into logical chunks that can be loaded when needed
  4. Helps manage dependencies efficiently

Without bundling, browsers would need to make numerous HTTP requests to load all the individual files that make up your application, significantly slowing down the initial load time.

How Angular Handles Bundling

Angular uses a build tool (Webpack under the hood in Angular CLI) to bundle your application. When you run ng build, Angular CLI processes your code through several steps:

  1. Compilation: Transforms TypeScript to JavaScript
  2. Bundling: Combines related files together
  3. Minification: Removes whitespace, renames variables, and optimizes code
  4. Tree-shaking: Eliminates unused code
  5. Chunk creation: Splits code into main bundle and feature-specific chunks
  6. Asset processing: Handles images, fonts, and other assets

Let's look at the default output of a production build:

bash
$ ng build --prod

Initial Chunk Files | Names | Raw Size | Estimated Transfer Size
main.1a2b3c4d5e6f.js | main | 213.4 kB | 56.8 kB
polyfills.2b3c4d5e6f.js | polyfills | 36.2 kB | 11.5 kB
runtime.3c4d5e6f7g.js | runtime | 2.4 kB | 1.2 kB
styles.4d5e6f7g8h.css | styles | 76.6 kB | 8.9 kB

| Initial Total | 328.6 kB | 78.4 kB

Each of these files serves a specific purpose:

  • main.js - Your application code
  • polyfills.js - Browser compatibility code
  • runtime.js - Webpack runtime to load other chunks
  • styles.css - Global styles extracted into a CSS file

Basic Bundling Strategies

1. Default Angular Bundling

By default, Angular CLI creates a reasonable bundling configuration. To build your app with standard optimization, run:

bash
ng build --prod

In Angular 12+ versions, you can also use:

bash
ng build --configuration production

2. Understanding Bundle Size

To analyze your bundle sizes, you can add the --stats-json flag to your build:

bash
ng build --configuration production --stats-json

Then use a tool like Webpack Bundle Analyzer to visualize the output:

bash
npm install -g webpack-bundle-analyzer
webpack-bundle-analyzer dist/your-project-name/stats.json

Advanced Bundling Techniques

1. Code-Splitting with Lazy Loading

One of the most effective ways to optimize your bundles is through lazy loading. Instead of loading the entire application at once, you can load features on-demand:

typescript
// In app-routing.module.ts
const routes: Routes = [
{
path: 'customers',
loadChildren: () => import('./customers/customers.module').then(m => m.CustomersModule)
},
{
path: 'orders',
loadChildren: () => import('./orders/orders.module').then(m => m.OrdersModule)
}
];

This creates separate bundles for each feature module that are loaded only when a user navigates to those routes.

2. Component-Level Code-Splitting

For large components that aren't immediately visible, you can use dynamic imports to load them when needed:

typescript
// Using dynamic import for a heavy component
import { Component } from '@angular/core';

@Component({
selector: 'app-dashboard',
template: `
<div>
<h1>Dashboard</h1>
<button (click)="loadHeavyComponent()">Load Heavy Component</button>
<div #container></div>
</div>
`
})
export class DashboardComponent {
async loadHeavyComponent() {
const { HeavyComponent } = await import('./heavy.component');
// Then use the component somehow (this would typically involve more code with ComponentFactoryResolver)
console.log('Component loaded:', HeavyComponent);
}
}

3. Differential Loading

Angular supports differential loading, which creates separate bundles for modern and legacy browsers:

json
// In tsconfig.json
{
"compilerOptions": {
"target": "es2015",
"module": "esnext",
// other options...
},
"angularCompilerOptions": {
// options...
}
}

With this configuration, modern browsers receive smaller, more optimized ES2015+ code, while older browsers get ES5 code with necessary polyfills.

4. Common Chunk Strategy

Control how shared code is bundled by modifying your Angular configuration:

json
// In angular.json
{
"projects": {
"your-app": {
"architect": {
"build": {
"options": {
// other options...
},
"configurations": {
"production": {
"optimization": true,
"commonChunk": true,
// other options...
}
}
}
}
}
}
}

Practical Example: Optimizing a Real-World App

Let's consider a hypothetical e-commerce application with several distinct areas:

  1. Product catalog
  2. Shopping cart
  3. User account
  4. Checkout process
  5. Order history

Before: Single Bundle Approach

All code loaded upfront, creating a large initial download:

typescript
// app-routing.module.ts (before optimization)
const routes: Routes = [
{ path: 'products', component: ProductCatalogComponent },
{ path: 'cart', component: ShoppingCartComponent },
{ path: 'account', component: UserAccountComponent },
{ path: 'checkout', component: CheckoutComponent },
{ path: 'orders', component: OrderHistoryComponent },
];

After: Optimized Bundling

Break the app into feature modules with lazy loading:

typescript
// app-routing.module.ts (optimized)
const routes: Routes = [
{
path: 'products',
loadChildren: () => import('./products/products.module').then(m => m.ProductsModule)
},
{
path: 'cart',
loadChildren: () => import('./cart/cart.module').then(m => m.CartModule)
},
{
path: 'account',
loadChildren: () => import('./account/account.module').then(m => m.AccountModule)
},
{
path: 'checkout',
loadChildren: () => import('./checkout/checkout.module').then(m => m.CheckoutModule)
},
{
path: 'orders',
loadChildren: () => import('./orders/orders.module').then(m => m.OrdersModule)
},
];

Each feature module would be structured like this:

typescript
// products/products.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router';
import { ProductListComponent } from './product-list.component';
import { ProductDetailComponent } from './product-detail.component';

@NgModule({
imports: [
CommonModule,
RouterModule.forChild([
{ path: '', component: ProductListComponent },
{ path: ':id', component: ProductDetailComponent }
])
],
declarations: [ProductListComponent, ProductDetailComponent]
})
export class ProductsModule { }

After implementing these changes, the initial bundle size might be reduced by 60-70%, significantly improving initial load time.

Advanced Configuration with Angular.json

You can further customize bundling behavior in your angular.json file:

json
{
"projects": {
"your-project": {
"architect": {
"build": {
"options": {
"optimization": true,
"outputHashing": "all",
"sourceMap": false,
"namedChunks": false,
"extractLicenses": true,
"vendorChunk": true,
"buildOptimizer": true,
"budgets": [
{
"type": "initial",
"maximumWarning": "2mb",
"maximumError": "5mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "6kb",
"maximumError": "10kb"
}
]
}
}
}
}
}
}

Key options include:

  • optimization: Enables optimizations like minification
  • outputHashing: Adds content hashes to filenames for cache busting
  • sourceMap: Controls source map generation
  • namedChunks: Names output chunks for debugging
  • vendorChunk: Creates a separate vendor bundle
  • buildOptimizer: Enables additional optimizations
  • budgets: Sets size limits that trigger warnings or errors

Monitoring Bundle Performance

After implementing bundling optimizations, it's important to measure their impact:

  1. Lighthouse: Run Lighthouse audits in Chrome DevTools to measure loading performance
  2. Network Panel: Use the browser's network panel to observe actual bundle loading
  3. Angular Budgets: Set up size budgets in your angular.json to be alerted when bundles exceed limits

Summary

Effective bundling is crucial for Angular application performance. By understanding and applying these bundling techniques, you can:

  • Reduce initial load time with lazy loading and code-splitting
  • Deliver optimized code for different browser capabilities
  • Improve perceived performance with smaller initial bundles
  • Better manage third-party dependencies

Remember that the optimal bundling strategy depends on your specific application needs. Start with Angular CLI's built-in optimizations, measure performance, and then apply more advanced techniques as needed.

Additional Resources

Exercises

  1. Analyze your current Angular application using Webpack Bundle Analyzer and identify the largest dependencies.
  2. Implement lazy loading for at least one feature module in your application.
  3. Set up differential loading and test your application in both modern and older browsers.
  4. Create a custom webpack configuration to optimize a specific third-party library.
  5. Implement component-level code splitting for a heavy component that isn't immediately visible on page load.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)