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.
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()providedIn: 'root'Yes (default)injectAsync()useFactory in providersfactory optionprovidedIn: 'any' etc.autoProvided: false@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 viainjectAsync().- Default export —
injectAsync()auto-unwrapsexport defaultclasses, so you don't need.then(m => m.default). prefetch: onIdle— Eagerly loads the service when the browser is idle viarequestIdleCallback. The service is ready before the user requests it.- Singleton — The resolved service is cached. Subsequent
awaitcalls return the same instance instantly.