injectAsync() (neu in v22) macht für Services, was @defer für Komponenten macht: Der Code des Services — und der der Bibliotheken, die er importiert — landet in einem separaten Chunk, der erst heruntergeladen wird, wenn du ihn wirklich brauchst. Perfekt für schwere, selten genutzte Features: PDF-/Excel-Export, Verschlüsselung, Datei-Parsing, große SDKs.
So funktioniert es
import { Component, injectAsync } from '@angular/core';
@Component({
selector: 'app-report-toolbar',
template: `<button (click)="exportPdf()">PDF exportieren</button>`,
})
export class ReportToolbar {
// Lädt jetzt NICHTS: gibt eine Funktion () => Promise<PdfExporter> zurück
private readonly pdf = injectAsync(() => import('./pdf-exporter').then((m) => m.PdfExporter));
async exportPdf() {
const exporter = await this.pdf(); // beim ersten Mal: lädt den Chunk herunter + erzeugt den Service
exporter.download(this.data()); // die folgenden Aufrufe: sofort, dieselbe Instanz
}
}Prefetch
import { injectAsync, onIdle } from '@angular/core';
// Prefetch, wenn der Browser im Leerlauf ist (requestIdleCallback)
readonly pdf = injectAsync(() => import('./pdf-exporter'), { prefetch: onIdle });
// Mit Timeout: im Leerlauf, spätestens aber nach 2 s
readonly charts = injectAsync(() => import('./charts'), { prefetch: () => onIdle({ timeout: 2000 }) });
// Dein eigener Trigger: jede Funktion, die ein Promise zurückgibt
readonly editor = injectAsync(() => import('./rich-editor'), {
prefetch: () => new Promise((resolve) => this.hovered.subscribe(() => resolve())),
});@defer vs. injectAsync vs. Lazy Routes
@defer injectAsync loadComponent (Route)
─────────────────────────────────────────────────────────────────────────────────────────
Was ist lazy Komponenten im Template Services (Logik) eine ganze Seite
Wann es lädt Trigger im Template beim ersten await bei der Navigation
Prefetch prefetch on ... { prefetch: onIdle } PreloadingStrategy
Beispiel Diagramm, Karte, Editor PDF, Excel, Krypto, /admin, /reports
Parser, große SDKsLadezustand und Fehler
Der erste Aufruf lädt einen Chunk herunter, kann also dauern und fehlschlagen. Behandle ihn wie einen Request: ein Signal für „lädt“, ein try/catch für den Fehler und ein finally, das den Indikator stoppt.
@Component({
selector: 'app-export-button',
template: `
<button (click)="exportPdf()" [disabled]="loading()">
{{ loading() ? 'Wird vorbereitet…' : 'PDF exportieren' }}
</button>
@if (failed()) {
<p role="alert">Der Export konnte nicht geladen werden. Lade die Seite neu und versuche es noch einmal.</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(); // beim ersten Mal wird der Chunk geladen, danach geht es sofort
await exporter.export(this.report());
} catch {
this.failed.set(true);
} finally {
this.loading.set(false);
}
}
}Was tatsächlich passiert
injectAsync() ist klein: Es merkt sich das Promise von import() und fragt nach dem Auflösen den Injector, in dem du die Funktion aufgerufen hast, nach der Klasse. Deshalb kann der Lazy-Service selbst inject() nutzen (etwa HttpClient), und ein Provider an der Komponente oder im Test hat Vorrang, genau wie bei inject().
Ein wichtiges Detail: Das Promise wird auch aufbewahrt, wenn es fehlschlägt. Scheitert der Download einmal (offline, ein neues Deployment hat den alten Chunk entfernt), schlägt jeder weitere Aufruf genauso fehl, bis die Seite neu geladen wird. Genauso, wenn das Prefetch scheitert: Der Fehler wird still ignoriert, aber der erste echte Aufruf bekommt ihn. Die Meldung an den Nutzer sollte daher das Neuladen der Seite vorschlagen, nicht „erneut versuchen“.
Testen
Weil das Ergebnis aus dem Injector kommt, kannst du im Test eine Fälschung für die Service-Klasse bereitstellen. Die Komponente bekommt die Fälschung, und die schwere Bibliothek wird gar nicht benutzt.
class FakePdfExporter {
readonly exported: unknown[] = [];
async export(report: unknown) {
this.exported.push(report);
}
}
it('exportiert, ohne die echte Bibliothek zu laden', async () => {
TestBed.configureTestingModule({
// injectAsync fragt den Injector nach der Klasse und bekommt deshalb die Fälschung
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));
});Dasselbe Prinzip für Komponenten ist @defer — Lazy Loading in Templates; wie du prüfst, was du im Bundle gespart hast, steht unter Performance — eine Checkliste.
Wann ja, wann nein
- Der Service importiert eine große Bibliothek (jspdf, xlsx, pdf.js, highlight, crypto).
- Das Feature wird nur von wenigen Nutzern oder selten verwendet (Export, Import, Admin).
- Du willst ein kleines initiales Bundle, ohne den Code in eine separate Route zu verschieben.
- Kleine, oft genutzte Services → ein normales
inject(). - Services, von denen das erste Rendering abhängt (Auth, Config) → sie würden die UI verzögern.
- Der Service wird anderswo bereits eager verwendet → er wird nicht mehr in einen Chunk ausgelagert.
Häufige Stolperfallen
- Der Service muss auto-provided sein (
@Service()/providedIn: 'root') — sonst gibt es keinen Ort, an dem er erzeugt werden kann. - Ein statischer Import an anderer Stelle: Wenn eine andere Datei
import { PdfExporter }macht (nicht nurimport type), landet der Code im Haupt-Bundle und das Lazy Loading bewirkt nichts. - Behandle Netzwerkfehler:
await this.pdf()kann fehlschlagen (offline). Pack es in eintry/catchund zeige eine Meldung an. - Injection-Kontext:
injectAsync()wird als Feld aufgerufen (wieinject()); die zurückgegebene Funktion kannst du überall aufrufen.
Spickzettel
// Lazy-Service mit benanntem Export
private readonly pdf = injectAsync(() => import('./pdf-exporter').then((m) => m.PdfExporter));
// mit Default-Export
private readonly pdf = injectAsync(() => import('./pdf-exporter'));
// vorab geladen, wenn der Browser frei ist
private readonly pdf = injectAsync(() => import('./pdf-exporter'), { prefetch: onIdle });
const exporter = await this.pdf(); // immer mit await; Fehler behandeln
import type { PdfExporter } from './pdf-exporter'; // anderswo nur import type, sonst wird der Chunk nicht mehr getrennt