This episode covers performance optimization: profiling with Angular DevTools, the Default versus OnPush change detection strategies, trackBy and lazy loading for code splitting, and bundle analysis and production optimization.

A feature-rich application is worthless if it loads slowly and feels heavy to use. Performance is part of the user experience, and Angular provides both the tools and the strategies to keep it fast.
Episode 15 covers profiling with Angular DevTools, the Default versus OnPush change detection strategies, trackBy and lazy loading for code splitting, and bundle analysis and production optimization. Optimize with data, not assumptions.
Angular DevTools is a browser extension for Chrome and Firefox. Once installed, open the Angular tab in DevTools while your application runs. There you'll find the Profiler panel, which records change detection activity.
ng.profiler.timeChangeDetection()ng.profiler.timeChangeDetection() in the console runs several change detection cycles and returns duration statistics. It's a quick way to find out whether the application is doing too much work when detecting changes.
The Profiler panel shows a flame chart — how long each component spends in change detection. Components that are thick and appear frequently are optimization candidates. Focus on the most expensive ones, not just the most numerous.
By default Angular checks all components when something changes. The more components, the more expensive it gets. The OnPush strategy limits checking to only when one of these conditions is met: an input changes, an event fires inside the component, or a signal changes.
import { Component, ChangeDetectionStrategy, signal } from '@angular/core';
@Component({
selector: 'app-item',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<p>{{ item().nama }}</p>`,
})
export class ItemComponent {
readonly item = signal({ nama: 'Kopi', harga: 25000 });
}ChangeDetectionStrategy.OnPush makes the component re-render only when it truly needs to. With signals, Angular knows exactly which dependencies changed — combining OnPush and signals produces a far more efficient application.
Default remains valid for small components that are rarely updated or that depend on many external factors. The rule of thumb: use OnPush as the default for new components, and measure the impact with the Profiler.
When a list changes, Angular must match old and new items. Without track, Angular uses object reference identity. With track, Angular uses a stable value so only the items that changed are re-rendered:
@for (produk of daftarProduk(); track produk.id) {
<app-kartu-produk [produk]="produk" />
}track produk.id tells Angular to track items by id instead of object reference. For classic lists, *ngFor uses trackBy. The impact is most noticeable on lists that are updated frequently or whose items can be reordered.
Code splitting breaks the bundle into per-feature chunks. In Angular, this is done with loadComponent on routes (episode 9):
{
path: 'laporan',
loadComponent: () =>
import('./laporan/laporan.component').then((m) => m.LaporanComponent),
}The report page's code is downloaded only when the user opens it. The initial load becomes lighter, and rarely visited pages don't burden first-time users.
Use source-map-explorer to see what's inside the bundle:
ng build --configuration=production
npx source-map-explorer dist/toko-online/browser/*.jssource-map-explorer shows which parts are biggest in the bundle — which libraries weigh it down. Common findings: large libraries used for only a few functions, or duplicated dependencies.
Set budgets in angular.json so the build fails when the bundle crosses a limit. Default budgets already exist; you can adjust them:
"budgets": [
{ "type": "initial", "maximumWarning": "500kb", "maximumError": "1mb" }
]With a maximumError: '1mb' budget, the build fails if the initial bundle exceeds 1 MB. This forces the team to keep the bundle size in check from the start, rather than chasing optimizations at the end.
Beyond budgets: enable optimization and hashing, consider lazy loading heavy libraries, avoid unused imports (tree shaking is already run by the build optimizer), and move large dependencies to the right page. Measure again after every change to confirm real improvements.
Key takeaways:
ng.profiler.timeChangeDetection() gives a quick picture of detection load.track on @for prevents re-rendering items that didn't change.loadComponent splits the bundle by the routes accessed.In the next episode, episode 16, we'll cover testing and quality — unit testing components and services, testing pipes, directives, and guards, integration testing with TestBed, and end-to-end testing with Cypress or Playwright.