Angular has always made it easy to lazy-load routes and components.
But what about services?
Imagine your application has an export feature that depends on a large library for generating spreadsheets or PDFs.
Only a small percentage of users click Export, but the code behind that feature may still end up being part of the initial JavaScript bundle.
Angular 22 introduces a simple solution:
injectAsync().
It allows you to load a service only when you actually need it. The API is stable since Angular 22.
The usual approach
Suppose we have a service responsible for exporting reports:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class ReportExporter {
export() {
// Generate spreadsheet, PDF, etc.
}
}
Normally, we could inject it into a component:
import { Component, inject } from '@angular/core';
import { ReportExporter } from './report-exporter';
@Component({
selector: 'app-report',
template: `
<button (click)="export()">
Export
</button>
`
})
export class ReportComponent {
private exporter = inject(ReportExporter);
export() {
this.exporter.export();
}
}
Simple and perfectly fine.
But if ReportExporter depends on a large library and the export feature is rarely used, we might prefer not to include that code in the initial load.
This is where injectAsync() becomes useful.
Enter injectAsync()
Instead of importing the service normally, we can load it dynamically:
import {
Component,
injectAsync
} from '@angular/core';
@Component({
selector: 'app-report',
template: `
<button (click)="export()">
Export
</button>
`
})
export class ReportComponent {
private exporter = injectAsync(
() =>
import('./report-exporter')
.then(m => m.ReportExporter)
);
async export() {
const exporter = await this.exporter();
exporter.export();
}
}
The important part is this:
private exporter = injectAsync(
() =>
import('./report-exporter')
.then(m => m.ReportExporter)
);
injectAsync() doesn't immediately return the service.
Instead, it returns a function that resolves to the service when called:
const exporter = await this.exporter();
The first call triggers the dynamic import.
Angular's bundler can therefore place the service in a separate JavaScript chunk instead of loading it with the initial application bundle. Subsequent calls reuse the same promise, so the chunk isn't downloaded again.
Conceptually, the loading process becomes:
Application starts
↓
Report page loads
↓
Exporter is NOT loaded
↓
User clicks Export
↓
Exporter chunk is downloaded
↓
Service is resolved through Angular DI
↓
Export runs
For features that most users never activate, this can be a useful performance optimization.
Your service must be auto-provided
There is one important requirement.
For lazy injection to work, Angular needs to know how to create the service automatically.
You can use:
@Injectable({
providedIn: 'root'
})
export class ReportExporter {
// ...
}
or Angular's newer @Service() decorator:
import { Service } from '@angular/core';
@Service()
export class ReportExporter {
// ...
}
A service that isn't auto-provided cannot be lazy-loaded with injectAsync().
You can also prefetch it
Sometimes you don't want the service in the initial bundle, but you also don't want the user to wait after clicking the button.
Angular provides a useful middle ground.
You can prefetch the dependency when the browser becomes idle:
import {
injectAsync,
onIdle
} from '@angular/core';
private exporter = injectAsync(
() =>
import('./report-exporter')
.then(m => m.ReportExporter),
{
prefetch: onIdle
}
);
The application can load normally first.
Then, when the browser has some idle time, Angular can start downloading the lazy dependency in the background.
If the user needs the feature before the prefetch happens, Angular simply loads it immediately.
When should you use it?
injectAsync() doesn't mean that every Angular service should become lazy.
For a small service used throughout the application, regular inject() is still simpler:
private userService = inject(UserService);
injectAsync() becomes more interesting when a service:
- depends on a large third-party library
- powers a rarely used feature
- handles exports or document generation
- loads specialized editors or visualization tools
- performs work that only some users need
In other words, use it when loading the service later can actually save meaningful JavaScript during application startup.
Final thought
injectAsync() is a small Angular 22 API, but it fills an interesting gap.
We already lazy-load routes.
We already lazy-load components.
Now we can apply the same idea to services:
const exporter = injectAsync(
() => import('./report-exporter')
);
And load expensive functionality only when the user actually needs it.
Sometimes improving startup performance isn't about making code execute faster.
It's simply about not loading that code yet.
Top comments (0)