injectAsync() (nouveau en v22) fait pour les services ce que @defer fait pour les composants : le code du service, et celui des bibliothèques qu’il importe, atterrit dans un chunk séparé, téléchargé uniquement quand vous en avez vraiment besoin. Idéal pour les fonctionnalités lourdes et rarement utilisées : export PDF/Excel, chiffrement, parsing de fichiers, gros SDK.
Fonctionnement
import { Component, injectAsync } from '@angular/core';
@Component({
selector: 'app-report-toolbar',
template: `<button (click)="exportPdf()">Exporter en PDF</button>`,
})
export class ReportToolbar {
// Elle ne charge RIEN maintenant : elle renvoie une fonction () => Promise<PdfExporter>
private readonly pdf = injectAsync(() => import('./pdf-exporter').then((m) => m.PdfExporter));
async exportPdf() {
const exporter = await this.pdf(); // la première fois : télécharge le chunk + crée le service
exporter.download(this.data()); // les appels suivants : instantanés, même instance
}
}Prefetch
import { injectAsync, onIdle } from '@angular/core';
// Prefetch quand le navigateur est inactif (requestIdleCallback)
readonly pdf = injectAsync(() => import('./pdf-exporter'), { prefetch: onIdle });
// Avec un timeout : à l’inactivité, mais au plus tard après 2 s
readonly charts = injectAsync(() => import('./charts'), { prefetch: () => onIdle({ timeout: 2000 }) });
// Votre propre déclencheur : toute fonction qui renvoie une Promise
readonly editor = injectAsync(() => import('./rich-editor'), {
prefetch: () => new Promise((resolve) => this.hovered.subscribe(() => resolve())),
});@defer vs injectAsync vs lazy routes
@defer injectAsync loadComponent (route)
─────────────────────────────────────────────────────────────────────────────────────────
Ce qui est lazy composants du template services (logique) une page entière
Quand il charge trigger dans le template au premier await à la navigation
Prefetch prefetch on ... { prefetch: onIdle } PreloadingStrategy
Exemple graphique, carte, éditeur PDF, Excel, chiffrement, /admin, /reports
parser, gros SDKÉtat de chargement et erreurs
Le premier appel télécharge un chunk : il peut prendre du temps et échouer. Traitez-le comme une requête : un signal pour « chargement », un try/catch pour l’erreur et un finally qui arrête l’indicateur.
@Component({
selector: 'app-export-button',
template: `
<button (click)="exportPdf()" [disabled]="loading()">
{{ loading() ? 'Préparation…' : 'Exporter en PDF' }}
</button>
@if (failed()) {
<p role="alert">Impossible de charger l’export. Rechargez la page et réessayez.</p>
}
`,
})
export class ExportButton {
private readonly pdf = injectAsync(() => import('./pdf-exporter').then((m) => m.PdfExporter));
protected readonly loading = signal(false);
protected readonly failed = signal(false);
protected async exportPdf() {
this.loading.set(true);
this.failed.set(false);
try {
const exporter = await this.pdf(); // la première fois, le chunk est téléchargé ; ensuite c’est instantané
await exporter.export(this.report());
} catch {
this.failed.set(true);
} finally {
this.loading.set(false);
}
}
}Ce qui se passe vraiment
injectAsync() est petit : il garde la promesse renvoyée par import() et, une fois résolue, demande la classe à l’injecteur dans lequel vous avez appelé la fonction. C’est pourquoi le service lazy peut lui-même utiliser inject() (par exemple HttpClient), et un provider sur le composant ou dans les tests a la priorité, exactement comme avec inject().
Un détail important : la promesse est conservée même quand elle échoue. Si le téléchargement échoue une fois (hors ligne, nouveau déploiement qui a supprimé l’ancien chunk), chaque appel suivant échoue de la même façon jusqu’au rechargement de la page. Pareil si le prefetch échoue : l’erreur est ignorée en silence, mais le premier vrai appel la reçoit. Le message à l’utilisateur doit donc proposer de recharger la page, pas « réessayer ».
Tests
Comme le résultat vient de l’injecteur, vous pouvez fournir un faux pour la classe du service dans les tests. Le composant reçoit le faux, et la bibliothèque lourde n’est pas utilisée du tout.
class FakePdfExporter {
readonly exported: unknown[] = [];
async export(report: unknown) {
this.exported.push(report);
}
}
it('exporte sans télécharger la vraie bibliothèque', async () => {
TestBed.configureTestingModule({
// injectAsync demande la classe à l’injecteur, il reçoit donc le faux
providers: [{ provide: PdfExporter, useClass: FakePdfExporter }],
});
const fixture = TestBed.createComponent(ExportButton);
fixture.nativeElement.querySelector('button').click();
const fake = TestBed.inject(PdfExporter) as unknown as FakePdfExporter;
await vi.waitFor(() => expect(fake.exported.length).toBe(1));
});Le même principe pour les composants, c’est @defer — Lazy Loading dans les templates ; comment vérifier ce que vous avez gagné dans le bundle : Performance — une checklist.
Quand OUI, quand NON
- Le service importe une grosse bibliothèque (jspdf, xlsx, pdf.js, highlight, crypto).
- La fonctionnalité est utilisée par peu d’utilisateurs ou rarement (export, import, admin).
- Vous voulez un petit bundle initial sans déplacer le code dans une route séparée.
- Petits services très utilisés → un
inject()normal. - Services dont dépend le premier rendu (auth, config) → ils retarderaient l’interface.
- Le service est déjà utilisé de façon eager ailleurs → il n’est plus isolé dans un chunk.
Pièges fréquents
- Le service doit être auto-provided (
@Service()/providedIn: 'root') : sinon, il n’y a aucun endroit d’où le créer. - Un import statique ailleurs : si un autre fichier fait
import { PdfExporter }(et pas seulementimport type), le code atterrit dans le bundle principal et le Lazy Loading ne sert à rien. - Gérez les erreurs réseau :
await this.pdf()peut échouer (hors ligne). Entourez-le d’untry/catchet affichez un message. - Contexte d’injection :
injectAsync()s’appelle comme un champ (commeinject()) ; la fonction renvoyée peut être appelée n’importe où.
Aide-mémoire
// service lazy, avec export nommé
private readonly pdf = injectAsync(() => import('./pdf-exporter').then((m) => m.PdfExporter));
// avec export default
private readonly pdf = injectAsync(() => import('./pdf-exporter'));
// téléchargé à l’avance, quand le navigateur est libre
private readonly pdf = injectAsync(() => import('./pdf-exporter'), { prefetch: onIdle });
const exporter = await this.pdf(); // toujours avec await ; gérez l’erreur
import type { PdfExporter } from './pdf-exporter'; // seulement import type ailleurs, sinon le chunk n’est plus séparé