Improving Angular Performance Scores By Over 150%

Improving Angular Performance Scores By Over 150%

5 min read
PerformanceAngularGoogle Core Web Vitals

The Challenge

When working with a major North American footwear retailer, their Angular e-commerce platform was facing severe performance issues that directly impacted user experience and conversion rates. The Core Web Vitals were well below recommended thresholds:

MetricOriginal ScoreTargetStatus
Largest Contentful Paint (LCP)8.2s≤2.5s❌ 327% over
Cumulative Layout Shift (CLS)0.45≤0.1❌ 350% over
First Input Delay (FID)320ms≤100ms❌ 220% over

These numbers translated to a poor user experience, with visible jank, delayed interactions, and likely revenue loss due to high bounce rates.

Optimization Strategy Overview

We took a systematic approach combining client-side Angular optimizations with server-side improvements. The strategy focused on:

  1. Reducing initial bundle size through smarter code splitting
  2. Improving rendering performance with change detection tweaks
  3. Stabilizing layout to eliminate CLS
  4. Implementing continuous monitoring with Lighthouse CI

Let's dive into each area.

1. Lazy Loading Revolution

The single biggest win came from implementing granular lazy loading for feature modules. Angular's default eager loading strategy meant users downloaded the entire application upfront, even for routes they never visited.

Custom Preloading Strategy

We created a custom preloading strategy that prioritizes critical modules while lazily loading others based on user interaction patterns:

@Injectable({ providedIn: 'root' })
export class CustomPreloadingStrategy implements PreloadingStrategy {
  preload(route: Route, load: () => Observable<any>): Observable<any> {
    // Preload critical modules immediately
    if (route.data && route.data['preload']) {
      return load();
    }

    // For other modules, wait until user hovers over related links
    return new Observable(observer => {
      // Track user navigation hints
      this.router.events
        .pipe(filter(event => event instanceof NavigationStart))
        .subscribe(() => observer.complete());

      // Optionally implement time-based preload after idle
      setTimeout(() => observer.next(), 5000);
    });
  }
}

// Route configuration
const routes: Routes = [
  {
    path: 'products',
    loadChildren: () => import('./products/products.module').then(m => m.ProductsModule),
    data: { preload: true } // critical path
  },
  {
    path: 'account',
    loadChildren: () => import('./account/account.module').then(m => m.AccountModule)
    // lazy load without preload
  }
];

And in the AppModule:

@NgModule({
  imports: [
    RouterModule.forRoot(routes, {
      preloadingStrategy: CustomPreloadingStrategy,
      scrollPositionRestoration: 'enabled',
      anchorScrolling: 'enabled'
    })
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

Impact: Initial bundle reduced from ~800KB to ~250KB gzipped, directly improving LCP by ~3 seconds.

2. Change Detection Optimization

Angular's default change detection strategy checks the entire component tree on every async event. For a complex e-commerce site, this was costly.

OnPush Strategy

We migrated all presentational components to ChangeDetectionStrategy.OnPush:

@Component({
  selector: 'app-product-card',
  templateUrl: './product-card.component.html',
  styleUrls: ['./product-card.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProductCardComponent {
  @Input() product: Product;
  @Input() discount: number;

  // Immutable updates ensure change detection works efficiently
  get discountedPrice(): number {
    return this.product.price * (1 - this.discount);
  }
}

This alone reduced change detection cycles by ~70% for interactive pages.

3. CLS (Cumulative Layout Shift) Mitigation

CLS was the hardest metric to tame due to third-party content and dynamic data. We implemented several strategies:

Aspect Ratio Boxes for Images & Videos

Instead of letting media elements size themselves after loading, we reserve space upfront:

<!-- Before -->
<img src="product.jpg" alt="Product" />

<!-- After -->
<div class="aspect-ratio-box">
  <img
    src="product.jpg"
    alt="Product"
    loading="lazy"
    width="800"
    height="600"
    style="aspect-ratio: 4/3;"
  />
</div>

With CSS:

.aspect-ratio-box {
  position: relative;
  width: 100%;
}
.aspect-ratio-box > img {
  position: absolute;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

CSS Containment

We added contain: layout style; to frequently updated sections to isolate repaints:

.product-grid {
  contain: layout style;
  will-change: transform;
}

.cart-drawer {
  contain: layout style paint;
}

Skeleton Loading States

Instead of showing blank areas while data loads, we render skeleton placeholders with the same dimensions as the final content.

<!-- Skeleton -->
<div *ngIf="loading" class="skeleton-product-card">
  <div class="skeleton-image"></div>
  <div class="skeleton-title"></div>
  <div class="skeleton-price"></div>
</div>

<!-- Real content -->
<div *ngIf="!loading" class="product-card">
  <img [src]="product.image" />
  <h3>{{ product.name }}</h3>
  <span>{{ product.price | currency }}</span>
</div>

4. Additional Performance Wins

Beyond the main areas, we also implemented:

  • TrackBy functions for all *ngFor loops to avoid unnecessary DOM re-renders
  • Virtual scrolling for long lists (using Angular CDK Scrolling)
  • WebP images with fallbacks, served via Cloudinary
  • Font display: swap to avoid blocking render
  • Server-side rendering (SSR) with Angular Universal for perceived performance
  • HTTP/2 push for critical assets
  • Aggressive caching headers via Cloudflare

5. Performance Monitoring with Lighthouse CI

To ensure performance stayed within acceptable ranges and to catch regressions early, we integrated Lighthouse CI into the GitHub Actions workflow.

Why Lighthouse CI?

  • Automated testing on every PR and push to main
  • Historical tracking via Lighthouse Server (self-hosted)
  • PR comments with visual performance trends
  • Commit statuses that block merging if scores drop below thresholds

GitHub Action Setup

We added .github/workflows/lighthouse-ci.yml (see file) which:

  1. Builds the Next.js app (actually Angular build, but conceptually similar)
  2. Installs and runs lhci autorun
  3. Collects Lighthouse results for all configured URLs
  4. Generates a markdown table summarizing performance, accessibility, best practices, and SEO scores with emojis to indicate health
  5. Posts the results as a comment on PRs
  6. Creates individual commit statuses for each page/route

The workflow uses the Lighthouse CI server for persistence (configured via LHCI_GITHUB_APP_TOKEN and LHCI_BUILD_CONTEXT). This allows us to track performance over time and compare against previous baselines.

Example PR Comment

When a PR is opened, the workflow automatically generates a table like:

URLPerformanceAccessibilityBest PracticesSEOReport
Home🟢 98 (989799)🟢 100🟢 100
Products🟡 82 (808185)🟢 98🟡 92

This immediate feedback helps reviewers consider performance implications before merging.

Manual Snapshots

We also use the workflow_dispatch trigger to run Lighthouse on demand, configured with a number_of_runs input (default 3) to get a reliable median.

Local Development

For local testing, we run:

npx lhci autorun

With a .lighthouserc.js that defines the URLs to test and the build command.

Results

After applying these optimizations and maintaining vigilance through CI, we achieved:

  • LCP: 8.2s → 2.1s (↓74%)
  • CLS: 0.45 → 0.12 (↓73%)
  • FID: 320ms → 89ms (↓72%)

All metrics now fall within Google's "Good" thresholds for Core Web Vitals. More importantly, we established a performance budget and automated guardrails to prevent regressions in future development cycles.

Conclusion

Performance improvements in Angular applications require a multi-faceted approach: smarter loading, efficient change detection, layout stability, and continuous monitoring. The combination of code-level optimizations and Lighthouse CI automation created a sustainable performance culture within the team.

If you're working on an Angular application and struggling with Core Web Vitals, start with the low-hanging fruit (lazy loading, OnPush) and then gradually introduce more advanced patterns. And don't forget to measure everything—what gets measured gets managed.

© 2026 Nathan Mathis