How I Cut My Angular App's Page Load Time by 48%
We’ve all been there: you build a feature-rich Angular application, but as the project grows, the Lighthouse score begins to tank. Users start complaining about "stuttering" on mobile, and the Largest Contentful Paint (LCP) starts creeping into the "red zone."
Recently, I took a bloated enterprise dashboard and managed to slash the page load time by nearly half (48%). I didn’t do it with a magic wand; I did it by systematically removing the "Angular Tax" and optimizing how we handle external resources.
Here is the blueprint of how I did it.
1. Optimizing the Critical Path: Preconnect
Before a single byte of my JavaScript bundle even executed, the browser was wasting time performing DNS lookups and TLS handshakes for our CDN-hosted assets (fonts, icons, and hero images).
By adding preconnect
hints to the <head>
, I told the browser to establish those connections early.
HTML<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://cdn.my-assets.com">The result: A measurable 200–400ms reduction in the "Time to First Byte" for external assets.
2. Upgrading to Modern Control Flow (@if
and @for
)
If you are still using *ngIf
and *ngFor
, you are essentially using "legacy" Angular. The new built-in control flow introduced in Angular 17+ isn't just syntactic sugar; it’s more performant.
Reduced Bundle Size: Because these are built-in, the compiler handles them directly without needing to import CommonModule
.
Better Type Checking: It’s faster for the developer and the browser.
Crucial Move: The track
Property In the old days, we often forgot trackBy
. In the new @for
syntax, a track
expression is mandatory.TypeScript@for (item of items; track item.id) {
<app-list-item [data]="item" />
}Using a unique ID for tracking prevents Angular from re-rendering the entire DOM list when only one item changes. This was a massive win for our data-heavy tables.
3. Architecture: Standalone Components & Lazy Loading
We moved away from the "One Giant AppModule" pattern. By converting to Standalone Components, we made our dependency graph visible to the compiler.
Lazy Loading Modules: We moved every route to a lazy-loaded pattern.
Component-Level Lazy Loading: Using the @defer
block (another modern Angular miracle), we deferred the loading of non-critical UI elements until the browser was idle or the user scrolled them into view.
4. Mastering Change Detection
The default "Check Always" strategy is the silent killer of Angular performance. We switched 90% of our components to ChangeDetectionStrategy.OnPush
.TypeScript@Component({
selector: 'app-stats-card',
standalone: true,
templateUrl: './stats-card.component.html',
changeDetection: ChangeDetectionStrategy.OnPush // 🚀 The secret sauce
})This tells Angular: "Don't check this component unless an @Input
changes or an observable emits." This stopped thousands of unnecessary "dirty checks" per second.
5. Runtime Loading for Heavy Third-Party Libraries
Our bundle was being held hostage by a 500KB Charting library and a massive Animation SDK. We stopped importing them at the top of the file. Instead, we started loading them only when needed using dynamic imports.TypeScriptasync loadCharts() {
const { Chart } = await import('chart.js');
// Initialize chart only after the code is downloaded at runtime
}This kept our initial main.js
bundle lean and mean.
6. Images: The Final Frontier
Images are almost always the largest part of a page. We implemented two fixes:
Lazy Loading: Added loading="lazy"
to all non-hero images.
Angular Image Directive: Used NgOptimizedImage
to enforce best practices like proper aspect ratios and automatic srcSet
generation.
HTML<img [ngSrc]="userProfilePic" width="50" height="50" priority>