injectAsync() (nou în v22) face pentru servicii ce face @defer pentru componente: codul serviciului — și al librăriilor pe care le importă — ajunge într-un chunk separat, descărcat abia când chiar ai nevoie de el. Perfect pentru funcționalități grele folosite rar: export PDF/Excel, criptare, parsare de fișiere, SDK-uri mari.
Cum funcționează
import { Component, injectAsync } from '@angular/core';
@Component({
selector: 'app-report-toolbar',
template: `<button (click)="exportPdf()">Exportă PDF</button>`,
})
export class ReportToolbar {
// NU încarcă nimic acum: întoarce o funcție () => Promise<PdfExporter>
private readonly pdf = injectAsync(() => import('./pdf-exporter').then((m) => m.PdfExporter));
async exportPdf() {
const exporter = await this.pdf(); // prima dată: descarcă chunk-ul + creează serviciul
exporter.download(this.data()); // următoarele apeluri: instant, aceeași instanță
}
}Prefetch
import { injectAsync, onIdle } from '@angular/core';
// Prefetch când browserul e liber (requestIdleCallback)
readonly pdf = injectAsync(() => import('./pdf-exporter'), { prefetch: onIdle });
// Cu timeout: idle, dar cel târziu după 2 s
readonly charts = injectAsync(() => import('./charts'), { prefetch: () => onIdle({ timeout: 2000 }) });
// Trigger propriu: orice funcție care întoarce un Promise
readonly editor = injectAsync(() => import('./rich-editor'), {
prefetch: () => new Promise((resolve) => this.hovered.subscribe(() => resolve())),
});@defer vs injectAsync vs rute lazy
@defer injectAsync loadComponent (rută)
─────────────────────────────────────────────────────────────────────────────────────────
Ce e lazy componente în template servicii (logică) o pagină întreagă
Când se încarcă trigger în template la primul await la navigare
Prefetch prefetch on ... { prefetch: onIdle } PreloadingStrategy
Exemplu grafic, hartă, editor PDF, Excel, criptare, /admin, /reports
parser, SDK-uri mariStarea de încărcare și erorile
Primul apel descarcă un chunk, deci poate dura și poate eșua. Tratează-l ca pe un request: un semnal pentru „se încarcă”, un try/catch pentru eroare și un finally care oprește indicatorul.
@Component({
selector: 'app-export-button',
template: `
<button (click)="exportPdf()" [disabled]="loading()">
{{ loading() ? 'Se pregătește…' : 'Exportă PDF' }}
</button>
@if (failed()) {
<p role="alert">Nu am putut încărca exportul. Reîncarcă pagina și încearcă din nou.</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(); // prima dată descarcă chunk-ul; apoi e instant
await exporter.export(this.report());
} catch {
this.failed.set(true);
} finally {
this.loading.set(false);
}
}
}Ce se întâmplă de fapt
injectAsync() e mic: ține minte promisiunea întoarsă de import(), iar când e gata cere clasa de la injectorul în care ai apelat funcția. De aceea serviciul lazy poate folosi la rândul lui inject() (de exemplu HttpClient), iar un provider pus pe componentă sau în teste are prioritate, exact ca la inject().
Un detaliu important: promisiunea e păstrată și când eșuează. Dacă descărcarea cade o dată (offline, deploy nou care a șters chunk-ul vechi), fiecare apel următor eșuează la fel, până la reîncărcarea paginii. La fel dacă eșuează prefetch-ul: eroarea e ignorată în tăcere, dar primul apel real o primește. Mesajul pentru utilizator ar trebui deci să sugereze reîncărcarea paginii, nu „încearcă din nou”.
Testare
Pentru că rezultatul vine din injector, în teste poți furniza un fals pentru clasa serviciului. Componenta primește falsul, iar librăria grea nu e folosită deloc.
class FakePdfExporter {
readonly exported: unknown[] = [];
async export(report: unknown) {
this.exported.push(report);
}
}
it('exportă fără să descarce librăria reală', async () => {
TestBed.configureTestingModule({
// injectAsync cere clasa de la injector, deci primește falsul
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));
});Același principiu, pentru componente, e @defer — lazy loading în template; cum verifici ce ai câștigat în bundle, la Performanță — checklist.
Când DA, când NU
- Serviciul importă o librărie mare (jspdf, xlsx, pdf.js, highlight, crypto).
- Funcționalitatea e folosită de puțini utilizatori sau rar (export, import, admin).
- Vrei bundle-ul inițial mic fără să muți codul într-o rută separată.
- Servicii mici, folosite des →
inject()normal. - Servicii de care depinde prima randare (auth, config) → ar întârzia UI-ul.
- Serviciul e deja folosit eager altundeva → nu se mai separă în chunk.
Capcane frecvente
- Serviciul trebuie să fie auto-provided (
@Service()/providedIn: 'root') — altfel nu are de unde fi creat. - Import static în altă parte: dacă un alt fișier face
import { PdfExporter }(nu doarimport type), codul ajunge în bundle-ul principal și lazy loading-ul nu mai are efect. - Tratează eroarea de rețea:
await this.pdf()poate eșua (offline). Pune-l într-untry/catchși arată un mesaj. - Context de injecție:
injectAsync()se apelează ca field (cainject()); funcția întoarsă poate fi apelată oriunde.
Fișă rapidă
// serviciu lazy, cu export numit
private readonly pdf = injectAsync(() => import('./pdf-exporter').then((m) => m.PdfExporter));
// cu export default
private readonly pdf = injectAsync(() => import('./pdf-exporter'));
// descărcat în avans, când browserul e liber
private readonly pdf = injectAsync(() => import('./pdf-exporter'), { prefetch: onIdle });
const exporter = await this.pdf(); // mereu cu await; tratează eroarea
import type { PdfExporter } from './pdf-exporter'; // doar import type în restul codului, altfel chunk-ul nu se mai separă