Tot ce face aplicația ta (JavaScript, change detection, layout, desenare) rulează pe un singur fir: main thread. Pentru o animație fluidă la 60 de cadre pe secundă, fiecare cadru are la dispoziție aproximativ 16 ms. Un calcul care durează 800 ms înseamnă 50 de cadre pierdute: butoanele nu răspund, inputul nu primește taste, animațiile îngheață.
Un Web Worker rulează cod JavaScript pe un alt fir, în paralel. Firul principal rămâne liber pentru interfață, iar cele două comunică prin mesaje. Angular CLI face un bundle separat pentru worker, scris în TypeScript și cu importuri normale.
@defer) nu te ajută când problema e un calcul lung. Aplicația e lentă pentru că firul principal e ocupat, nu pentru că randează prea mult. Exemplele tipice: parsarea unui CSV mare, filtrarea a sute de mii de rânduri, generarea unui PDF, procesarea de imagini, criptarea.Firul principal, în direct
Demo-ul rulează același cod de numărare a numerelor prime în două locuri. Bulina e mișcată din JavaScript, cu requestAnimationFrame, deci se oprește exact când firul principal e blocat. O animație CSS ar fi putut continua pe compositor și ar fi ascuns problema. „Cel mai lung cadru” arată cât a stat interfața înghețată.
Cum creezi un worker
ng generate web-worker configurează proiectul și creează scheletul. Practic, totul se reduce la new Worker(new URL('./x.worker', import.meta.url), { type: 'module' }). Forma new URL(..., import.meta.url) e cea pe care builder-ul o recunoaște: o tratează ca punct de intrare separat și îi dă un bundle propriu. Exact așa e construit demo-ul de mai sus, fără nicio altă configurare. În fișierul worker-ului, /// <reference lib="webworker" /> aduce tipurile corecte (postMessage, self).
ng generate web-worker app/reports/report
# configurează proiectul pentru workers (dacă nu e deja) și creează report.worker.tsCe poate și ce nu poate un worker
| Poate | Nu poate |
|---|---|
| calcul pur, cod TypeScript importat normal | accesa DOM-ul (document, elemente) |
fetch, IndexedDB, timere, WebAssembly | folosi DI-ul Angular, componente sau servicii ale aplicației |
primi și trimite date prin postMessage | citi starea aplicației: primește doar ce îi trimiți |
Worker-ul ascuns într-un serviciu
Componentele n-ar trebui să știe de postMessage. Un serviciu face legătura: trimite cererea cu un id, asociază răspunsul și întoarce un Promise. Așa se integrează direct cu resource() (vezi resource), iar rezultatul devine un semnal cu isLoading și error. Pe server (SSR) și în unele medii de test nu există Worker; documentația Angular menționează explicit @angular/platform-server. Pune un fallback sincron (vezi Cod compatibil cu serverul).
import { DestroyRef, Injectable, inject } from '@angular/core';
/** Ascunde detaliile worker-ului: restul aplicației vede o funcție async. */
@Injectable({ providedIn: 'root' })
export class ReportWorker {
private worker?: Worker;
private nextId = 0;
private readonly pending = new Map<number, (r: Report) => void>();
constructor() {
inject(DestroyRef).onDestroy(() => this.worker?.terminate());
}
build(input: ReportInput): Promise<Report> {
if (typeof Worker === 'undefined') return Promise.resolve(buildReport(input)); // SSR / teste: fallback
this.worker ??= this.create();
const id = ++this.nextId;
return new Promise((resolve) => {
this.pending.set(id, resolve);
this.worker!.postMessage({ id, input });
});
}
private create(): Worker {
const w = new Worker(new URL('./report.worker', import.meta.url), { type: 'module' });
w.onmessage = ({ data }) => {
this.pending.get(data.id)?.(data.report);
this.pending.delete(data.id);
};
return w;
}
}Copiere vs transfer
Mesajele sunt copiate prin structured clone: obiectele, array-urile, Map, Date și buffer-ele sunt duplicate în memoria celuilalt fir. Pentru date mari, copierea poate costa mai mult decât calculul. Un ArrayBuffer poate fi în schimb transferat: proprietatea trece la worker instant, iar buffer-ul tău devine gol (byteLength === 0).
const pixels = new Uint8ClampedArray(1920 * 1080 * 4); // ~8 MB
// Copiere (structured clone): browserul dublează datele — cost proporțional cu mărimea
worker.postMessage(pixels.buffer);
// Transfer: proprietatea trece la worker, fără copiere; bufferul tău devine „detached”
worker.postMessage(pixels.buffer, [pixels.buffer]);
console.log(pixels.buffer.byteLength); // 0| Mod | Dus-întors | buffer.byteLength după trimitere |
|---|---|---|
| Trimite un buffer în ambele moduri și compară. | ||
Când DA, când NU
- Calcule continue de peste ~50 ms: parsare CSV/JSON mare, agregări, filtrare și sortare pe sute de mii de rânduri.
- Procesare de imagini, audio, compresie, criptare, hashing.
- Biblioteci grele care nu ating DOM-ul (diff-uri de text, validări complexe, WebAssembly).
- Polling sau calcule care ar concura cu animațiile și inputul.
- Calcule de câteva milisecunde: mesajele și pornirea worker-ului costă mai mult.
- Lucru cu DOM-ul sau randare: worker-ul nu are acces. Optimizează randarea (vezi virtual scroll, performanță).
- Date uriașe care merg dus-întors prin copiere. Costul de clonare anulează câștigul; folosește transfer sau procesează pe server.
- Request-uri HTTP obișnuite: sunt deja asincrone și nu blochează firul.
Capcane frecvente
- Worker creat la fiecare click și nefinalizat. Fiecare are propriul fir și propria memorie. Refolosește-l și apelează
terminate()la distrugere. - Lipsa lui
onerror: o excepție în worker nu ajunge în ErrorHandler-ul aplicației. Tratează erorile explicit și trimite-le înapoi ca mesaj. - Instanțe de clase trimise prin mesaj: structured clone păstrează datele, nu prototipul. O
class Moneyajunge obiect simplu, fără metode. - Animații CSS ca „dovadă” că UI-ul merge: pot rula pe compositor chiar dacă JavaScript-ul e blocat. Testează cu un input.
- Fără fallback la SSR:
new Workerpe server aruncă eroare.
Fișă rapidă
CREARE new Worker(new URL('./x.worker', import.meta.url), { type: 'module' })
ng generate web-worker <cale> — configurează proiectul + scaffold
ÎN WORKER /// <reference lib="webworker" /> · addEventListener('message', ...) · postMessage(...)
fără DOM, fără Angular DI, fără window — doar calcul și fetch
MESAJE structured clone (copiere) — date simple, Map, Set, Date, ArrayBuffer...
transfer: postMessage(buf, [buf]) — instant, bufferul tău devine gol
ÎN APLICAȚIE serviciu care întoarce Promise/Observable → resource() / toSignal()
terminate() la final; onerror pentru erori
SSR typeof Worker === 'undefined' pe server → fallback sincron
MERITĂ > ~50 ms de calcul continuu: parsare, compresie, criptare, filtrare de date mari, imagini
NU MERITĂ calcule mici, lucru cu DOM-ul, date uriașe copiate dus-întors fără transfer