High-impact rules for building fast, scalable, and maintainable Angular applications focused on performance optimization.
Your users abandon sites that take longer than 3 seconds to load. Your Angular app probably takes 5-8 seconds. These optimization rules fix that problem permanently.
You've built feature-rich Angular applications, but you're watching users bounce because:
Every second of delay costs you 7% conversion rate. Every laggy interaction pushes users toward faster alternatives.
These rules transform your Angular development workflow around measurable performance targets: Largest Contentful Paint under 2.5s, Interaction to Next Paint under 200ms, and Total Blocking Time under 200ms.
Instead of retrofitting performance optimizations, you'll build performance-first from day one:
// Before: Heavy component causing 16ms+ change detection cycles
@Component({
selector: 'user-list',
template: `
<div *ngFor="let user of users">
{{ formatUserName(user) }} - {{ calculateScore(user) }}
</div>
`
})
// After: Optimized component with sub-16ms detection cycles
@Component({
standalone: true,
selector: 'user-list',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div *ngFor="let user of displayUsers; trackBy: trackById">
{{ user.displayName }} - {{ user.score }}
</div>
`
})
export class UserListComponent {
displayUsers = computed(() =>
this.users().map(user => ({
...user,
displayName: this.formatUserName(user),
score: this.calculateScore(user)
}))
);
trackById = (index: number, item: User) => item.id;
}
Before: Ship 2.5MB bundles and hope CDNs save you
ng build
# Main bundle: 2.1MB
# Vendor bundle: 1.8MB
# Total: 3.9MB
After: Automated performance budgets fail builds over thresholds
// angular.json performance budgets
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
}
]
// Tree-shaken, lazy-loaded result:
// Main bundle: 180KB
// Lazy chunks: 45KB average
// Total initial: 225KB
Before: Default change detection strategy rerenders everything
// Every component triggers full tree re-evaluation
// 500 components × 16ms = 8 second freeze
After: OnPush strategy with signals eliminates unnecessary renders
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `{{ userSignal().name }}`
})
// Only updates when userSignal actually changes
// 5ms detection cycles, 60fps maintained
Before: Client-side only rendering with loading spinners
// User sees blank screen for 3-5 seconds
// Hydration doubles HTTP requests
// SEO crawlers see empty content
After: Universal SSR with hydration and transfer state
// Pre-rendered HTML loads in 800ms
// Data transfers from server to client seamlessly
// Perfect SEO with actual content
Development Speed: Stop context-switching between performance tools. Build-time bundle analysis and automated Lighthouse CI catch regressions before they ship.
Debugging Time: OnPush components with pure functions eliminate 80% of "why isn't this updating?" debugging sessions.
Feature Velocity: Lazy loading architecture lets you ship features independently without impacting core app performance.
Team Confidence: Performance budgets in CI mean no more "is this too slow?" conversations during code review.
# Update angular.json for production-grade builds
ng build --configuration=production --build-optimizer --aot
// Batch convert components to OnPush
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
// ... rest of component
})
// Use takeUntilDestroyed() for subscriptions
export class MyComponent {
private destroy$ = inject(DestroyRef);
ngOnInit() {
this.dataService.getData()
.pipe(takeUntilDestroyed(this.destroy$))
.subscribe(data => this.processData(data));
}
}
// Convert eager modules to lazy routes
const routes: Routes = [
{
path: 'dashboard',
loadComponent: () => import('./dashboard/dashboard.component')
},
{
path: 'users',
loadChildren: () => import('./users/users.module')
}
];
# Add Lighthouse CI to your pipeline
npm install -D @lhci/cli
# Fail builds below performance thresholds
lhci autorun --upload.target=filesystem
// Move calculations out of templates
// Before: {{ calculateTotal(items) }}
// After: Pre-computed in component
readonly totalSignal = computed(() =>
this.items().reduce((sum, item) => sum + item.value, 0)
);
Week 1: Bundle sizes drop 60-70%, initial load times cut from 5s to 1.8s
Week 2: Change detection optimizations eliminate frame drops, interactions feel instant
Week 3: SSR implementation improves SEO rankings and user retention by 23%
Week 4: Automated performance budgets prevent regressions, team ships confidently
Bottom Line: Users stop complaining about speed. Conversion rates increase. Your Angular app becomes the fast one users prefer.
These rules don't just optimize your current app—they rewire how you think about Angular development. Performance becomes automatic, not an afterthought.
Ready to build Angular apps that load instantly and run smoothly? Your users are waiting.
You are an expert in Angular 17+, TypeScript 5+, RxJS 7+, HTML, SCSS, Node.js, Vite/Webpack, and modern browser APIs.
Key Principles
- Prioritize measurable performance (largest-contentful-paint <2.5 s, interaction-to-next-paint <200 ms).
- Ship the smallest possible JavaScript: tree-shake, lazy-load, and compress everything.
- Embrace immutability and pure functions; minimize side-effects for easier change-detection.
- Prefer declarative, functional, and reactive patterns (RxJS, Signals).
- Fail fast: validate inputs and throw early to keep the “happy path” flat.
- Automate performance budgets in CI; fail the build if bundles exceed thresholds.
- Optimize for first time-to-interaction (FTTI) first, then for runtime throughput.
TypeScript
- Always compile with "strict": true. Treat any implicit any as an error.
- Interfaces over classes for models; use readonly to communicate immutability.
- Prefer const, then let; never use var.
- Favor union types and discriminated unions instead of enums where possible.
- Re-export types from an index.ts to keep import paths flat.
- File naming: kebab-case (e.g., user-profile.model.ts). Class & component names in PascalCase.
- Never suppress errors with // @ts-ignore. Refactor instead.
- Example – typed HTTP service:
```ts
export interface ApiError { status: number; message: string; }
export interface UserDto { id: string; name: string; }
export const getUser$ = (http: HttpClient, id: string) =>
http.get<UserDto>(`/api/users/${id}`)
.pipe(catchError((err): Observable<never> => throwError((): ApiError => ({
status: err.status,
message: err.error?.message ?? 'Unknown error'
}))));
```
Error Handling & Validation
- Guard clauses at the top of every function/component.
- Centralize HTTP errors with HttpInterceptor; map raw errors to domain-specific error objects.
- Provide a GlobalErrorHandler that logs (Sentry/etc.) and shows a user-friendly toast/dialog.
- In RxJS streams, always terminate with catchError and NEVER swallow errors.
- Validate all @Input() values with runtime checks in development builds.
- Return typed left/right (Result) or throw custom errors; never return undefined for failure.
Angular Framework Rules
- Use Standalone Components, Directives, Pipes; avoid NgModules unless needed for libraries.
- Change Detection
• Default strategy: OnPush for every component (add `changeDetection: ChangeDetectionStrategy.OnPush`).
• For heavy list rendering, combine OnPush + *ngFor trackBy.
- Compilation & Bundling
• Enable AOT and build optimizer (`ng build --configuration=production`).
• Activate differential loading and ESBuild-based minification.
- Routing & Code-Splitting
• Implement Lazy Loading for feature routes via `loadComponent`/`loadChildren`.
• Leverage PreloadAllModules or custom preloading only when bandwidth idle.
• Use Module Federation for micro-frontends if multiple teams share code.
- State Management
• Use Signals or ComponentStore for local state; NgRx/Akita only for cross-cutting/global state.
• Store Observables in the template with the `| async` pipe; never subscribe manually in the component unless you unsubscribe in `takeUntilDestroyed()`.
- Templates
• No function calls or complex expressions; pre-compute in the component.
• Prefer `@input() set` + signals to derive UI-ready data.
• Use `ng-container` to avoid extra DOM.
- Rendering Optimization
• Virtual scrolling via CDK for lists > 250 items.
• Defer heavy components with `ngOptimizedImage`, `ng-lazyload`, or deferred loaders.
- Server-Side Rendering
• Use Angular Universal + hydration. Pre-render static routes (`ng run project:prerender`).
• Stream data using TransferState to avoid duplicate HTTP calls on the client.
- Web Workers
• Offload CPU-intensive tasks (`generatePdf`, `imageResize`) via `ng g web-worker task`.
Performance Toolbox
- Angular DevTools > Profiler tab: locate change detection cycles >16 ms.
- `ng build --stats-json` + source-map-explorer to locate large bundles.
- Lighthouse CI: budget LCP <2.5 s, Total Blocking Time <200 ms.
- Chrome Performance panel: ensure 60 fps scrolling; main-thread tasks <50 ms.
Testing
- Unit: Jasmine + Karma (CLI default) or Jest (faster); include performance budget tests with jest-dom.
- Component: Angular Testing Library; prefer `render()` over TestBed.createComponent.
- E2E: Cypress + lighthouse metrics (web-vitals) per PR.
- Use `ng test --watch=false --configuration=ci` in pipelines.
Security
- Enable strict CSP headers; whitelist only required scripts.
- Sanitize HTML with DomSanitizer; never bypass security trusts unless absolutely necessary.
- Keep Angular & dependencies patched (`npx npm-check-updates -u`).
Directory / File Structure
- `src/`
• `app/feature/` — lazy route folder
• `app/shared/` — pure UI components & pipes
• `core/` — interceptors, guards, global services
• `assets/` — images, i18n
• `environments/` — env.ts files (no secrets!)
- Keep each feature ≤ 2 seconds LCP budget.
CI/CD
- Fail build if `npm run lighthouse` score < 90.
- Use `--build-optimizer --vendor-chunk --common-chunk --named-chunks=false` flags.
Common Pitfalls
- Forgetting trackBy → list rerenders on every change.
- Mutating @Input() arrays/objects → forces full change detection.
- Heavy logic inside template pipes → runs each CD cycle.
- Not unsubscribing from manual subscriptions → memory leaks.
Sample Component Skeleton
```ts
@Component({
standalone: true,
selector: 'user-card',
templateUrl: './user-card.html',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [NgOptimizedImage],
})
export class UserCardComponent {
readonly user = injectSignal(UserStore.selectUser());
trackById = (index: number, item: User) => item.id;
}
```
Enforce these rules in code reviews and set up automated linters (ESLint, Stylelint, Husky pre-commit) to ensure continuous compliance.