Angular

Performance

15 Questions

Ahead-of-Time (AOT) compilation converts Angular HTML and TypeScript into JavaScript during the build step, rather than at runtime. This provides faster rendering because templates are pre-compiled. It also enables smaller bundles, fewer async requests, and catches template errors at build time. AOT is the default in production builds.
ng build --configuration production // AOT is default in production
AOT catches template errors like typos in property names before deployment. JIT (Just-in-Time) compiles at runtime and is used only during development.

Why it matters: Tests understanding of Angular build optimization. Shows you know compilation strategies.

Real applications: Production builds use AOT by default for faster rendering and smaller bundles.

Common mistakes: Developers don't understand AOT is default in production. They use JIT in production by mistake. They don't know template errors are caught at build time with AOT.

Tree shaking is a build optimization that eliminates unused code from the final bundle. The build tool analyzes import/export chains and removes code that is never referenced. Angular's providedIn: 'root' enables tree-shakable services where unused services are automatically removed. This significantly reduces bundle size.
// Tree-shakable service: removed if never injected
@Injectable({ providedIn: 'root' })
export class UnusedService { } // removed from bundle

// Not tree-shakable: always included
providers: [AlwaysIncludedService]
Use providedIn: 'root' instead of adding services to providers arrays. Import only what you need from libraries for better tree shaking.

Why it matters: Tree shaking reduces bundle size significantly. Shows you know build optimization techniques.

Real applications: Large applications with many unused services benefit from tree shaking removing hundreds of KB of code.

Common mistakes: Developers don't use providedIn: 'root' losing tree shaking benefits. They import entire libraries when only one function is needed. They don't understand static analysis limitations.

trackBy tells Angular how to identify items in a list, preventing unnecessary DOM destruction and re-creation. Without trackBy, Angular destroys and recreates all DOM elements when the array reference changes. With trackBy, Angular reuses existing DOM elements for items with the same identity. This dramatically improves performance for large lists.
<li *ngFor="let item of items; trackBy: trackById">{{ item.name }}</li>

trackById(index: number, item: Item): number {
  return item.id; // Angular reuses DOM for items with same id
}
Always use trackBy for lists that change frequently or contain many items. The track function should return a unique identifier like an id or key.

Why it matters: trackBy prevents DOM thrashing in large lists. Shows you know list rendering optimization.

Real applications: Tables with thousands of rows, infinite scrolling lists, real-time updating dashboards.

Common mistakes: Developers forget trackBy entirely. They use array index as identifier (wrong for sorted/filtered lists). They use complex tracking functions that trigger unnecessary re-renders.

Lazy loading defers loading of feature modules or components until they are actually needed, reducing initial bundle size and startup time. The code is split into separate chunks that are loaded on demand when the user navigates to a route. This means users only download the code they need. Both NgModule-based and standalone approaches support lazy loading.
{ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) }
// or standalone
{ path: 'admin', loadComponent: () => import('./admin.component').then(c => c.AdminComponent) }
Lazy loading is one of the most impactful performance optimizations. Combine with preloading strategies for instant navigation after initial load.

Why it matters: Lazy loading dramatically reduces initial bundle size. Shows you know code splitting strategies.

Real applications: Large applications with admin interfaces, premium features, or extensive feature sets all benefit from lazy loading.

Common mistakes: Developers don't use lazy loading so large bundles slow initial load. They over-eagerly load everything. They don't combine with preloading for performance.

Virtual scrolling (CDK) only renders items that are currently visible in the viewport, dramatically improving performance for large lists. Instead of rendering thousands of DOM elements, it creates just enough to fill the visible area. As the user scrolls, elements are recycled and re-rendered with new data. The itemSize specifies the height of each item in pixels.
<cdk-virtual-scroll-viewport itemSize="50" class="viewport">
  <div *cdkVirtualFor="let item of items">{{ item.name }}</div>
</cdk-virtual-scroll-viewport>
Import ScrollingModule from @angular/cdk/scrolling. Virtual scrolling can handle lists with hundreds of thousands of items smoothly.

Why it matters: Virtual scrolling enables rendering massive lists efficiently. Shows you know advanced scrolling optimization.

Real applications: Grids with millions of rows, real-time data feeds, large list interfaces.

Common mistakes: Developers render all DOM elements for large lists causing browser freeze. They don't use virtual scrolling when needed. They set incorrect itemSize causing layout issues.

Use the --stats-json flag to generate a webpack statistics file, then visualize it with webpack-bundle-analyzer. This generates a treemap showing which modules and dependencies are largest. It helps identify opportunities for lazy loading, removing unused dependencies, and reducing bundle bloat.
ng build --stats-json
npx webpack-bundle-analyzer dist/my-app/stats.json
The analyzer shows a visual treemap of your bundle. Look for unexpectedly large dependencies and consider dynamic imports or lighter alternatives.

Why it matters: Bundle analysis identifies bloat and optimization opportunities. Shows you can profile and optimize builds.

Real applications: Large applications use bundle analysis to find unexpected large dependencies or unused imports.

Common mistakes: Developers don't analyze bundles assuming they're optimized. They import everything from large libraries when only a function is needed. They don't remove unused dependencies.

Preloading strategies load lazy modules in the background after the initial app has loaded. This gives the bundle size benefits of lazy loading with the instant navigation of eager loading. PreloadAllModules preloads everything. Custom strategies or QuicklinkStrategy preload only visible or likely routes.
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
// or QuicklinkStrategy for only visible links
Preloading happens during idle time so it does not affect initial load performance. Use custom strategies to prioritize which modules to preload first.

Why it matters: Preloading balances bundle size with navigation speed. Shows you know advanced routing optimization.

Real applications: Best of both worlds: small initial bundle plus instant navigation after load completes.

Common mistakes: Developers use PreloadAllModules on small apps wasting resources. They don't create custom strategies for larger apps. They don't understand preloading is only for idle time.

The async pipe automatically subscribes and unsubscribes from Observables, preventing memory leaks. With OnPush change detection, it calls markForCheck() automatically, enabling efficient updates. It eliminates the need to manually manage subscriptions in the component.
// Template: auto-subscribes and unsubscribes
<div *ngIf="data$ | async as data">
  <p>{{ data.name }}</p>
</div>

// Component: no subscribe/unsubscribe needed
data$ = this.http.get<User>('/api/user');
The async pipe is the recommended way to consume Observables in templates. It reduces boilerplate and prevents common memory leak bugs.

@defer enables declarative lazy loading of template blocks in Angular 17+. The deferred content loads only when a trigger condition is met like viewport visibility, user interaction, or a timer. It automatically splits deferred components into separate bundles. Define placeholder, loading, and error states for a smooth experience.
@defer (on viewport) {
  <app-heavy-chart [data]="data" />
} @placeholder {
  <div>Loading chart...</div>
} @loading (minimum 500ms) {
  <app-spinner />
}
@defer triggers include on viewport, on interaction, on hover, on idle, and on timer. It reduces initial bundle size by deferring heavy components.

Why it matters: @defer enables declarative code splitting. Shows you know template-level lazy loading strategies.

Real applications: Heavy charts, data grids, modals, and below-the-fold content benefit from deferred rendering.

Common mistakes: Developers don't use @defer leaving heavy components in initial bundle. They defer components that should load eagerly. They don't use placeholder/loading states for good UX.

NgOptimizedImage directive optimizes image loading with lazy loading, priority hints, and srcset for responsive images. It enforces best practices by requiring width and height attributes to prevent layout shift. Non-priority images automatically get loading="lazy". Mark above-the-fold images with the priority attribute.
<img ngSrc="hero.jpg" width="400" height="300" priority />
NgOptimizedImage can integrate with image CDNs for automatic format conversion and resizing. Using it correctly improves Core Web Vitals scores.

Why it matters: NgOptimizedImage enforces best practices automatically. Shows you know image performance optimization.

Real applications: E-commerce sites, galleries, content platforms all depend on optimized image loading for fast performance.

Common mistakes: Developers use regular img tags without lazy loading causing performance issues. They don't set width/height causing layout shift. They don't mark priority images for preloading.

@defer enables declarative lazy loading of template sections. The deferred content loads only when a specified trigger condition is met like viewport visibility, user interaction, or a timer. This reduces the initial bundle size because deferred components are split into separate chunks. Define placeholder, loading, and error states.
@defer (on viewport) {
  <app-heavy-chart [data]="chartData" />
} @placeholder {
  <div>Chart will load here</div>
} @loading (minimum 500ms) {
  <app-spinner />
} @error {
  <p>Failed to load chart</p>
}
The @placeholder shows before loading starts. The @loading block appears during the download. minimum prevents flicker by ensuring the loading state shows for a minimum duration.

Why it matters: @defer gives you template-level control over code splitting. Shows you know modern performance patterns.

Real applications: Modal dialogs, tabs, nested features, and conditional UI benefit from declarative lazy loading.

Common mistakes: Developers use component lazy routes when @defer is simpler. They don't understand @defer creates separate chunks. They don't combine @defer with other optimizations.

Pure pipes (default) are only re-evaluated when the input value changes by reference. Angular caches the result and reuses it as long as the input reference stays the same. This is very efficient because change detection runs frequently but the pipe transform only executes when needed. Impure pipes run on every change detection cycle.
// Pure pipe: only runs when 'value' reference changes
@Pipe({ name: 'expensive', pure: true })
export class ExpensivePipe implements PipeTransform {
  transform(value: Data[]): Data[] {
    return value.filter(item => item.active).sort((a, b) => a.name.localeCompare(b.name));
  }
}
Using pure pipes with immutable data patterns gives you free memoization. Always prefer pure pipes and create new array/object references when data changes.

Why it matters: Pure pipes enable automatic memoization. Shows you know caching and optimization patterns.

Real applications: Complex data transformations like sorting, filtering, formatting benefit from pure pipe caching.

Common mistakes: Developers use impure pipes (pure: false) adding performance overhead. They mutate arrays/objects breaking memoization. They create new transforms inside impure pipes repeatedly.

Angular Universal enables SSR where the application is pre-rendered on the server into HTML before being sent to the browser. This improves First Contentful Paint because users see content immediately without waiting for JavaScript to download. It also improves SEO because search engines can crawl the pre-rendered HTML.
// Angular 17+ SSR setup
ng add @angular/ssr

// app.config.server.ts
const serverConfig: ApplicationConfig = {
  providers: [
    provideServerRendering()
  ]
};
After the initial render, Angular hydrates the page on the client side, attaching event listeners and making it interactive. SSR is essential for content-heavy and SEO-dependent applications.

Why it matters: SSR improves performance and SEO significantly. Shows you know full-stack optimization techniques.

Real applications: Blogs, news sites, e-commerce platforms, and content-driven applications depend on SSR for performance and SEO.

Common mistakes: Developers don't use SSR for content apps losing SEO benefits. They use SSR everywhere including admin dashboards (unnecessary). They don't handle browser-only APIs (localStorage) in SSR code.

NgOptimizedImage enforces image loading best practices automatically. It adds lazy loading for non-priority images, generates proper srcset attributes for responsive images, and prevents layout shift by requiring width and height. It marks above-the-fold images as priority for preloading.
import { NgOptimizedImage } from '@angular/common';

@Component({
  standalone: true,
  imports: [NgOptimizedImage],
  template: '<img ngSrc="hero.jpg" width="800" height="400" priority />' +
    '<img ngSrc="photo.jpg" width="400" height="300" />'
})
export class GalleryComponent { }
It can integrate with image CDNs for automatic optimization and format conversion. Using it correctly improves Core Web Vitals scores significantly.

Why it matters: NgOptimizedImage automates best practices enforcement. Shows you know image loading optimization.

Real applications: Photo galleries, e-commerce product images, content platforms, and hero sections benefit from NgOptimizedImage.

Common mistakes: Developers don't use ngSrc forgetting lazy loading benefits. They forget width/height attributes causing cumulative layout shift. They don't mark LCP images with priority attribute.

Lazy loading defers loading of a module or component until the user actually navigates to its route, which can cause a small delay. Preloading loads lazy modules in the background after the initial application has loaded, so they are already available when the user navigates. Preloading gives the bundle size benefits of lazy loading with the instant navigation of eager loading.
// Lazy loading: loaded only on navigation
{ path: 'admin', loadComponent: () => import('./admin.component') }

// With preloading: loaded in background after app starts
RouterModule.forRoot(routes, {
  preloadingStrategy: PreloadAllModules
})
Use PreloadAllModules for small apps or a custom strategy for larger apps to prioritize which modules to preload first.

Why it matters: Understanding both lazy loading and preloading is essential for bundle optimization. Shows you know available tradeoffs.

Real applications: Large applications balance these strategies: lazy load features with low priority and preload likely navigation paths.

Common mistakes: Developers use PreloadAllModules for all apps losing bundle size benefits. They don't create custom strategies for real optimization. They don't monitor network patterns before choosing preloading strategies.