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:
- Combines many small JavaScript files into fewer, larger files
- Applies optimizations like minification and tree-shaking
- Organizes code into logical chunks that can be loaded when needed
- 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:
- Compilation: Transforms TypeScript to JavaScript
- Bundling: Combines related files together
- Minification: Removes whitespace, renames variables, and optimizes code
- Tree-shaking: Eliminates unused code
- Chunk creation: Splits code into main bundle and feature-specific chunks
- Asset processing: Handles images, fonts, and other assets
Let's look at the default output of a production build:
$ 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:
ng build --prod
In Angular 12+ versions, you can also use:
ng build --configuration production
2. Understanding Bundle Size
To analyze your bundle sizes, you can add the --stats-json
flag to your build:
ng build --configuration production --stats-json
Then use a tool like Webpack Bundle Analyzer to visualize the output:
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:
// 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:
// 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:
// 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:
// 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:
- Product catalog
- Shopping cart
- User account
- Checkout process
- Order history
Before: Single Bundle Approach
All code loaded upfront, creating a large initial download:
// 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:
// 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:
// 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:
{
"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:
- Lighthouse: Run Lighthouse audits in Chrome DevTools to measure loading performance
- Network Panel: Use the browser's network panel to observe actual bundle loading
- 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
- Angular Official Guide on Deployment
- Webpack Bundle Analyzer
- Angular Performance Checklist
- The Cost of JavaScript in 2019
Exercises
- Analyze your current Angular application using Webpack Bundle Analyzer and identify the largest dependencies.
- Implement lazy loading for at least one feature module in your application.
- Set up differential loading and test your application in both modern and older browsers.
- Create a custom webpack configuration to optimize a specific third-party library.
- 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! :)