Angular 22

injectAsync , Lazy DI

Angular 22 introduces injectAsync() for asynchronous dependency injection. Services are loaded only when needed, keeping the initial bundle small. Combine with onIdle to prefetch during idle time.

1. On-Demand Loading

AnalyticsService is not in the initial bundle. It's loaded the first time you click a tracking button. Subsequent calls reuse the already-loaded service.

private loadAnalytics = injectAsync(
  () => import('./services/analytics.service')
           .then(m => m.AnalyticsService)
);

async trackEvent(name: string) {
  const analytics = await this.loadAnalytics();
  analytics.trackEvent(name);
}

2. Default Export Support

MarkdownRendererService uses export default. injectAsync() automatically unwraps it — no .then(m => m.default) needed.

Preview

Click "Render" to load the service and preview.

// MarkdownRendererService uses `export default`
private loadMarkdown = injectAsync(
  () => import('./services/markdown-renderer.service')
);
// No .then(m => m.default) needed!

3. Prefetch with onIdle

PdfExportService is prefetched when the browser is idle using { prefetch: onIdle }. By the time you click "Export", the service is likely already loaded — resulting in near-instant execution.

private loadPdfExport = injectAsync(
  () => import('./services/pdf-export.service')
           .then(m => m.PdfExportService),
  { prefetch: onIdle }  // loads when browser is idle
);

4. @Service() vs @Injectable()

Angular 22 introduces @Service() as a simpler alternative to @Injectable({ providedIn: 'root' }). It's auto-provided by default and enables lazy loading via injectAsync().

@Injectable()@Service()
Auto-providedOnly with providedIn: 'root'Yes (default)
Lazy loadingNot supportedVia injectAsync()
Custom factoryVia useFactory in providersfactory option
Scoped (not global)providedIn: 'any' etc.autoProvided: false
Boilerplate@Injectable({ providedIn: 'root' })@Service()

Basic Comparison

// @Injectable ,  the classic way (still works)
@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);
  getUser(id: number) { return this.http.get(`/api/users/${id}`); }
}

// @Service ,  the Angular 22 way (simpler!)
@Service()
export class UserService {
  private http = inject(HttpClient);
  getUser(id: number) { return this.http.get(`/api/users/${id}`); }
}

@Service() Options

// Auto-provided (default) ,  equivalent to providedIn: 'root'
@Service()
export class AnalyticsService { }

// NOT auto-provided ,  must be added to providers manually
@Service({ autoProvided: false })
export class ScopedService { }

// With factory ,  custom instantiation logic
@Service({
  factory: () => {
    const http = inject(HttpClient);
    const config = inject(APP_CONFIG);
    return new ApiClient(http, config.apiUrl);
  }
})
export class ApiClient { }

Lazy Loading (@Service only)

// @Service enables lazy loading via injectAsync()
// This is NOT possible with @Injectable

// analytics.service.ts
@Service()
export class AnalyticsService {
  trackEvent(name: string) { /* ... */ }
}

// component.ts
private loadAnalytics = injectAsync(
  () => import('./analytics.service').then(m => m.AnalyticsService)
);

async track() {
  const svc = await this.loadAnalytics();
  svc.trackEvent('click');
}

How injectAsync Works

  • Lazy by default — The service module is not included in the initial bundle. It's fetched over the network on first use.
  • @Service() — New Angular 22 decorator. Auto-provides the service (like @Injectable({ providedIn: 'root' })) and enables lazy loading via injectAsync().
  • Default export — injectAsync() auto-unwraps export default classes, so you don't need .then(m => m.default).
  • prefetch: onIdle — Eagerly loads the service when the browser is idle via requestIdleCallback. The service is ready before the user requests it.
  • Singleton — The resolved service is cached. Subsequent await calls return the same instance instantly.