AngularAtlased. 22.2

Cap. 16UI & performanță avansată71 / 75

Memory leaks: cum le găsești și le repari

Subscripții, timere și listeneri care trăiesc mai mult decât componenta lor — cum îi vezi în DevTools și cum îi închizi.

Un memory leak într-o aplicație Angular rareori strică ceva. Tabul doar devine mai lent și mai greu pe parcursul unei după-amiezi: de fiecare dată când o componentă e distrusă, ceva pornit de ea continuă să ruleze — o subscripție, un timer, un listener pe window — și ține întreaga componentă în viață odată cu el. Plătesc utilizatorii care lasă aplicația deschisă toată ziua.

Pagina asta arată sursele din spatele aproape oricărui leak, cum închizi fiecare sursă și cum le găsești pe cele scăpate cu Chrome DevTools. Laboratorul creează leak-uri reale în acest tab și le curăță când părăsești pagina.

De ce contează
Distrugerea unei componente nu oprește ce a pornit ea. Angular scoate DOM-ul și rulează hook-urile de destroy; orice e referit dintr-un loc care trăiește mai mult — un serviciu root, un timer, un listener global — încă arată spre componentă și spre tot ce arată componenta.

Suspecții obișnuiți

Widget-ul ăsta pare inofensiv și conține cele trei leak-uri clasice. Fiecare callback capturează this și fiecare e ținut de ceva care trăiește mai mult decât widget-ul: un serviciu root, lista de timere a browserului, window. Deschide-l și închide-l de câteva ori în laborator și urmărește ce rămâne în urmă.

@Component({ selector: 'app-price-widget', template: `{{ price() }}` })
export class PriceWidget implements OnInit {
  private readonly prices = inject(PriceFeed); // un serviciu root: trăiește cât aplicația
  protected readonly price = signal(0);

  ngOnInit() {
    // ✗ 1. feed-ul ține callback-ul, callback-ul ține `this`, pentru totdeauna
    this.prices.updates$.subscribe((p) => this.price.set(p));

    // ✗ 2. timer-ul continuă să ruleze după ce componenta a dispărut
    setInterval(() => this.refresh(), 5000);

    // ✗ 3. window trăiește mai mult decât orice componentă
    window.addEventListener('resize', () => this.layout());
  }
}
LiveDeschide și închide un widget care uită să facă curat
Închide la destroy:

Niciun widget pe pagină.

distruse, încă vii
0
memorie reținută
0.00 MB
muncă de zombie
0/s
subscripții
0
timere
0
listeneri
0
widget-uri create
0
Apasă „Deschide și închide de 10 ori”, apoi mișcă mouse-ul pe pagină: widget-urile distruse continuă să lucreze. Bifează cele trei căsuțe, repetă, iar contoarele rămân la zero.

Fiecare widget ține un buffer de 256 KB, deci numărul „reținut” e memorie care chiar nu poate fi eliberată cât timp trăiește zombie-ul ei. Căsuțele se aplică widget-urilor create după ce le bifezi; „Curăță zombie-urile” oprește ce au lăsat în urmă cele vechi.

Cum închizi fiecare sursă

Regula e scurtă: pentru tot ce pornești, înregistrezi un stop, chiar în momentul în care pornești. Pentru observables, Angular are două unelte care fac asta pentru tine: toSignal() și takeUntilDestroyed(); pentru rest există DestroyRef.onDestroy(). Listenerii declarați în host sunt scoși de Angular; pentru mai mulți listeneri pe obiecte globale, un AbortController îi închide pe toți într-o linie.

import { Component, DestroyRef, inject, signal } from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-price-widget',
  template: `{{ price() }}`,
  host: { '(window:resize)': 'layout()' }, // ✓ 3. Angular scoate singur host listener-ele
})
export class PriceWidget {
  private readonly prices = inject(PriceFeed);
  private readonly destroyRef = inject(DestroyRef);

  // ✓ 1a. toSignal se dezabonează când componenta e distrusă
  protected readonly price = toSignal(this.prices.updates$, { initialValue: 0 });

  constructor() {
    // ✓ 1b. sau, când chiar ai nevoie de subscribe(): takeUntilDestroyed
    this.prices.alerts$.pipe(takeUntilDestroyed()).subscribe((a) => this.notify(a));

    // ✓ 2. fiecare timer își are „stop”-ul înregistrat lângă „start”
    const id = setInterval(() => this.refresh(), 5000);
    this.destroyRef.onDestroy(() => clearInterval(id));
  }
}
SursăCe o ține în viațăRemediul
subscribe()sursa ține callback-ul tău, callback-ul ține componentatoSignal(), pipe-ul async sau takeUntilDestroyed()
setIntervalbrowserul ține callback-ul până când timer-ul e opritDestroyRef.onDestroy(() => clearInterval(id))
addEventListenerwindow și document trăiesc mai mult decât orice componentăhost: { '(window:resize)': … }, Renderer2.listen sau un AbortController
cache-uri în serviciiun serviciu root trăiește cât aplicația, la fel și Map-ul luiștergi intrarea la destroy sau folosești un WeakMap cu elementul drept cheie
widget-uri third-partygraficele, hărțile și editoarele își înregistrează propriii listeneri și timerele apelezi destroy() / remove() în onDestroy
// ✗ un serviciu root care ține minte componente (sau DOM-ul lor) le ține în viață
@Service()
export class Tooltips {
  private readonly open = new Map<string, TooltipComponent>(); // nu se șterge niciodată → leak
}

// ✓ ștergi la destroy sau lași garbage collector-ul să decidă, cu un WeakMap
@Service()
export class Tooltips {
  private readonly anchors = new WeakMap<HTMLElement, TooltipComponent>(); // cheia dispare → intrarea dispare
}

Ce nu face leak

Nu orice subscribe() e o problemă. Cererile HttpClient se completează singure după răspuns (cu takeUntilDestroyed(), o cerere lentă e și anulată când pleci). Pipe-ul async, toSignal(), output() și host listener-ele fac curat după ele, la fel și effect() și computed() create într-o componentă. Router-ul curăță observables-urile din ActivatedRoute. Ce face leak e o subscripție la ceva care nu se completează niciodată și trăiește mai mult decât tine: un store, un Subject dintr-un serviciu root, interval, fromEvent(window, …), evenimentele router-ului.

Cum găsești leak-uri cu Chrome DevTools

Un leak e un număr care crește când ar trebui să rămână constant. Metoda: repeți o acțiune care n-ar trebui să lase nimic în urmă — deschizi și închizi un dialog, mergi pe o pagină și înapoi — și compari memoria înainte și după.

  1. Mai sus, în laborator, debifează cele trei reparații. Deschide DevTools → panoul Memory → la Select profiling type alege Heap snapshot. Apasă iconița de coș (Collect garbage), apoi Take snapshot.
  2. Apasă Deschide și închide de 10 ori și așteaptă să se termine. Din nou Collect garbage și Take snapshot.
  3. Cu Snapshot 2 selectat, schimbă meniul din stânga sus din Summary în Comparison (comparat cu Snapshot 1) și sortează după # Delta.
  4. Scrie în Filter by class app-leaky-widget: +10, câte un element host pentru fiecare widget închis. Apoi DOMTimer (+10 timere încă pornite) și EventListener (+10 listeneri). Pe site-ul publicat numele claselor sunt minificate, așa că LeakyWidget apare doar când rulezi aplicația în development.
  5. Dă click pe o instanță și citește panoul Retainers de jos: desfă rândurile ca să urci lanțul până la ce o ține în viață. Apasă Curăță zombie-urile, fă un al treilea snapshot și vei vedea aceleași rânduri la # Deleted.
  6. Mai direct: la Select profiling type alege Detached elements și apasă Get detached elements. Apare câte un <app-leaky-widget> pentru fiecare widget închis.

Pentru o verificare rapidă fără snapshot-uri: meniul de comenzi (Ctrl/Cmd+Shift+P) → Show Performance monitor. Urmărește JS event listeners: crește cu 10 la fiecare „de 10 ori” cu reparațiile debifate și nu mai scade până nu cureți.

Toate panourile, pas cu pas, cu exerciții și capcanele (de exemplu DevTools care ține el însuși obiecte în viață), sunt la DevTools: cum te uiți în aplicație.

Un test care prinde leak-ul data viitoare

După ce ai reparat un leak, fă-l imposibil de readus. Un Subject știe dacă mai e cineva abonat (observed); după fixture.destroy() răspunsul trebuie să fie false. Mai multe despre teste în testarea cu Vitest.

import { TestBed } from '@angular/core/testing';
import { describe, expect, it } from 'vitest';

describe('PriceWidget', () => {
  it('stops listening to the feed when destroyed', () => {
    const feed = TestBed.inject(PriceFeed); // updates$ e un Subject în serviciu
    const fixture = TestBed.createComponent(PriceWidget);
    fixture.detectChanges();
    expect(feed.updates.observed).toBe(true);

    fixture.destroy();
    expect(feed.updates.observed).toBe(false); // nu mai ascultă nimeni
  });
});

Când DA, când NU

Merită vânate
  • Taburi care stau deschise ore întregi: dashboard-uri, unelte de back-office, puncte de vânzare, chat.
  • Componente create și distruse des: dialoguri, rânduri de listă, taburi, rute pe care revii des.
  • Cod care se abonează la lucruri globale: store-uri, WebSockets, evenimente window, evenimente de router.
Nu merită grija
  • O cerere HttpClient unică: se completează singură.
  • Subscripții prin pipe-ul async sau toSignal(): sunt curățate pentru tine.
  • Un serviciu root abonat la ceva pe toată durata aplicației: asta e intenționat, nu un leak.

Capcane frecvente

  • subscribe() în ngOnInit, fără teardown, la ceva dintr-un serviciu root.
  • Încredere în take(1). Dacă sursa nu emite niciodată — utilizatorul pleacă înainte de prima valoare — subscripția așteaptă la nesfârșit.
  • removeEventListener cu altă funcție. .bind(this) creează de fiecare dată o funcție nouă, așa că nu se scoate nimic.
  • Componente sau noduri DOM într-un Map dintr-un serviciu. Serviciul trăiește la nesfârșit, deci și ele.
  • Măsurători fără garbage collection înainte. Heap-ul urcă și coboară singur; compară snapshot-uri făcute după Collect garbage.

Fișă rapidă

subscribe() la o sursă longevivă     takeUntilDestroyed() · toSignal() · pipe-ul async
take(1) / first()                    sigur doar dacă sursa VA emite; altfel așteaptă la nesfârșit
setInterval / lanțuri de setTimeout  DestroyRef.onDestroy(() => clearInterval(id))
listeneri pe window / document       host: { '(window:resize)': … } · Renderer2.listen · AbortController
removeEventListener                  are nevoie de ACEEAȘI funcție: păstrezi referința, nu .bind() de două ori
servicii root                        niciun Map de componente sau noduri DOM; ștergi la destroy sau WeakMap
widget-uri third-party               chart.destroy(), map.remove() în onDestroy
nu face leak                         HttpClient (se completează), pipe-ul async, toSignal, outputs, host listeners
tot sigure                           effect() și computed() în componente: distruse odată cu ele
DevTools                             Memory → snapshot → repeți de N× → GC → snapshot → Comparison
citește retainers                    Subscriber / Timeout / EventListener = cine îl ține în viață
"Detached"                           noduri DOM scoase din pagină, dar încă referite
în CI                                subject.observed === false după fixture.destroy()

Cap. 16 · 0 din 4 terminate

Ai terminat lecția?

Marcheaz-o și apare pe harta ta: în cuprins și pe prima pagină.

Se marchează singură după ce citești toată lecția.

  1. Web Workers
  2. Liste uriașe: virtual scroll
  3. Animații moderne & View Transitions
  4. Memory leaks: cum le găsești și le repari
Vezi toată harta ta

Toate cele 75 de pagini · scrie ca să filtrezi

  • 01Ce e nou în Angular 21 & 22Toate schimbările importante din v21 și v22, pe scurt, cu link către pagina care le explică.Start
  • 01Prima ta aplicație, pas cu pasO listă de lecturi construită de la o componentă goală la o aplicație terminată — signals, @for, input-uri și output-uri, un serviciu și un formular, pas cu pas.Start
  • 01Decizii rapide„Unde pun asta?”, „Signal sau Observable?”, „Shared sau feature?” — răspunsuri scurte la întrebările de zi cu zi.Start
  • 01Întrebări de interviu Angular60 de întrebări reale de interviu pentru junior, mid și senior, cu răspunsul de dat, întrebarea care urmează și greșelile de evitat. Plus o simulare de interviu.Start
  • 02Standalone & bootstrapCum pornește o aplicație fără NgModule: bootstrapApplication, app.config.ts, imports în componentă.Fundamente moderne
  • 02Data binding și evenimenteInterpolare, binding pe proprietăți și pe atribute, evenimente cu $event și filtre de taste și cum e construit [( )] — ce intră în DOM și ce se întoarce.Fundamente moderne
  • 02Control flow: @if, @for, @switchSintaxa de blocuri care înlocuiește *ngIf/*ngFor/ngSwitch, cu track, @empty și verificare exhaustivă.Fundamente moderne
  • 02@let și expresiile noi din templateVariabile locale cu @let, arrow functions, spread, regex, instanceof, comentarii în tag-uri.Fundamente moderne
  • 02@defer — lazy loading în templateÎncarci componente grele doar când e nevoie: triggere, prefetch, placeholder, loading, error.Fundamente moderne
  • 02Class & style bindingsDe ce [class.x] și [style.x] au înlocuit ngClass și ngStyle (cu migrare automată din v21).Fundamente moderne
  • 02Stilurile componentelor și încapsularea:host, cele trei moduri de încapsulare, teme cu variabile CSS și de ce ::ng-deep e ultima soluție.Fundamente moderne
  • 02Pipes: când și cumPipe pur vs impur, pipe custom și cum alegi între pipe, computed() și o metodă.Fundamente moderne
  • 03signal() și computed()Primitivele reactive: stare scriibilă, stare derivată, egalitate, untracked și reguli de imutabilitate.Signals & reactivitate
  • 03effect() — când DA, când NUEfecte secundare sincronizate cu semnale: localStorage, logging, API-uri imperative. Și capcanele lor.Signals & reactivitate
  • 03linkedSignal() — stare derivată editabilăUn semnal care se resetează când se schimbă sursa, dar pe care îl poți și seta manual.Signals & reactivitate
  • 03resource, rxResource, httpResourceDate asincrone ca semnale: status, value, error, reload, anulare automată. Stabile din v22.Signals & reactivitate
  • 03debounced() — semnale cu întârziereVarianta „debounce” a unui semnal, fără RxJS. Perfect pentru căutare.Signals & reactivitate
  • 03Liste: căutare, filtrare, sortare, paginareUn lanț de computed-uri care recalculează doar ce s-a schimbat, cu filtrele ținute în URL, ca un link să redeschidă aceeași vedere.Signals & reactivitate
  • 03Observables de la zeroCe e un Observable, subscribe și unsubscribe, operatorii pe care îi folosești în fiecare săptămână, cold vs hot — și când ajunge un signal.Signals & reactivitate
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — și când RxJS rămâne alegerea bună.Signals & reactivitate
  • 04input() și output()Inputuri ca semnale (required, transform, alias) și outputuri tipizate. Data down, events up.Componente: comunicare
  • 04model() — two-way bindingBanana-in-a-box [( )] cu un singur API. Când îl folosești și când e mai bine input + output.Componente: comunicare
  • 04viewChild & contentChildInterogări ca semnale: viewChild, viewChildren, contentChild, contentChildren, opțiunea read.Componente: comunicare
  • 04Lifecycle & DestroyRefCe hook-uri mai contează în era signals, afterNextRender, DestroyRef și ngOnChanges tipizat.Componente: comunicare
  • 04Content projectionng-content cu select, sloturi multiple, conținut implicit și capcana proiecției condiționale.Componente: comunicare
  • 04ng-template & ngTemplateOutletTemplate-uri ca valori: render props în Angular, context tipizat și personalizare de componente.Componente: comunicare
  • 05Smart vs DumbContainer vs presentational: cine ține starea, cine doar afișează, și cum le împarți corect.Design de componente
  • 05Componente generice <T>Un DataTable<T> sau Select<T> tipizat corect, cu template-uri pentru celule.Design de componente
  • 05Componente dinamiceNgComponentOutlet, createComponent cu inputBinding/outputBinding și un dashboard construit din config.Design de componente
  • 05Directive & host compositionDirective atribut, metadata host: {} și Directive Composition API (hostDirectives).Design de componente
  • 05Controale custom de formularFormValueControl (Signal Forms) vs ControlValueAccessor: un rating cu stele, în ambele variante.Design de componente
  • 05Change detectionOnPush e implicit din v22, zone.js lipsește din v21. Ce declanșează randarea și cum să nu te arzi.Design de componente
  • 06inject() & @Service()Noul decorator @Service() din v22, inject() vs constructor și contextul de injecție.Dependency Injection
  • 06Providers & ierarhia de injectoriRoot vs rută vs componentă, useClass/useValue/useFactory/useExisting și modificatorii optional/self/skipSelf.Dependency Injection
  • 06InjectionToken & provideX()Configurare tipizată, funcții provideFeature(), makeEnvironmentProviders și inițializatori.Dependency Injection
  • 06injectAsync() — servicii lazyÎncarci un serviciu greu abia când ai nevoie de el, cu prefetch la idle.Dependency Injection
  • 07Structura de folderecore / shared / features, ce intră în fiecare și cum arată un feature pe dinăuntru.Arhitectura aplicației
  • 07Shared vs featureCând muți o componentă în shared și (mai ales) când NU. Checklist concret.Arhitectura aplicației
  • 07Interfețe & modeleInterfață inline sau fișier separat? DTO vs model de domeniu, type vs interface, unde le pui.Arhitectura aplicației
  • 07State management cu signalsStore-uri simple pe semnale, facade, stare locală vs globală și când îți trebuie NgRx.Arhitectura aplicației
  • 07Style guide & namingConvențiile oficiale actuale: nume de fișiere fără sufixe, protected, readonly, ordinea membrilor.Arhitectura aplicației
  • 07Barrels, aliasuri & granițeindex.ts, path aliases, API public pe feature și reguli de dependență care se pot verifica automat.Arhitectura aplicației
  • 07Erori: @boundary & ErrorHandlerNoul bloc @boundary din v22.2, ErrorHandler global, erori HTTP și erori din resource.Arhitectura aplicației
  • 08Routing modern & lazy loadingloadComponent, rute copil, parametri ca input-uri și moștenirea parametrilor (default nou în v22).Routing
  • 08Guards & resolverscanActivate, canMatch, canDeactivate și resolve ca funcții, cu redirect prin UrlTree / RedirectCommand.Routing
  • 09Signal Forms — bazeleFormulare construite pe semnale: form(), [formField], validare prin schemă. Stabile din v22.Formulare
  • 09Signal Forms — avansatValidare custom și cross-field, async/HTTP cu debounce, liste dinamice, disabled/hidden, scheme reutilizabile.Formulare
  • 09Reactive Forms tipizateFormGroup tipizat, nonNullable, FormArray — și cum conviețuiesc cu Signal Forms.Formulare
  • 10HttpClient, interceptors & API layerHttpClient fără setup (v21), Fetch implicit (v22), interceptori funcționali și stratul de API cu mapare DTO.HTTP & date
  • 11Performanță — checklistCe chiar contează: OnPush + signals, track, @defer, lazy routes, imagini, bundle și hidratare.Calitate & performanță
  • 11Testare cu VitestVitest e runner-ul implicit din v21. TestBed, componente cu semnale, servicii, HTTP și harness-uri.Calitate & performanță
  • 11DevTools: cum te uiți în aplicațieChrome DevTools și Angular DevTools pas cu pas: ce arată fiecare panou, cu exerciții care merg chiar pe site-ul ăsta.Calitate & performanță
  • 11Angular AriaComponente headless, accesibile, pe care le stilizezi cum vrei: tabs, accordion, menu, listbox...Calitate & performanță
  • 12SSR, prerender & render modesCând randezi pe server, când la build și când doar în browser: RenderMode pe rută, app.routes.server.ts și costurile reale.SSR & hidratare
  • 12Hidratare: event replay & incrementalCum preia browserul HTML-ul de pe server fără să-l refacă: hidratare completă, event replay, @defer (hydrate on …) și erorile de nepotrivire.SSR & hidratare
  • 12Cod compatibil cu serverulwindow nu există pe server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, cache-ul HTTP și PendingTasks.SSR & hidratare
  • 13Micro frontendsCând merită să spargi o aplicație în mai multe deploy-uri și când e doar complexitate: Native Federation, Module Federation, iframes, Web Components.Arhitectură la scară
  • 13Web Components cu Angular ElementscreateCustomElement: o componentă Angular folosită ca tag HTML oriunde, plus comunicarea între aplicații prin evenimente și BroadcastChannel.Arhitectură la scară
  • 13Monorepo, librării & granițeUn workspace cu mai multe aplicații și librării: ng generate library, Nx, entry points secundare, publicare și reguli de dependență.Arhitectură la scară
  • 13Arhitectură hexagonală (ports & adapters)Domeniul fără Angular, porturi ca clase abstracte și adaptoare interschimbabile: HTTP, localStorage sau in-memory, fără să atingi UI-ul.Arhitectură la scară
  • 14Semnalele pe dinăuntruGraful reactiv: producători și consumatori, push-pull, de ce computed e lazy și glitch-free, problema diamantului și când rulează efectele.Reactivitate avansată
  • 14RxJS avansat: operatori de ordin superiorswitchMap, mergeMap, concatMap și exhaustMap pe o diagramă live, plus retry cu backoff, shareReplay și cum eviți scurgerile.Reactivitate avansată
  • 14NgRx SignalStoreState management la scară pe semnale: withState, withComputed, withMethods, rxMethod, entități și hooks — și când un store simplu e suficient.Reactivitate avansată
  • 14Mașini de stări cu signalsFără stări imposibile: o mașină tipizată pe signals, cu tranziții, guards și diagrama stărilor, live.Reactivitate avansată
  • 15WebSockets: timp real care rezistă rețeleiUn socket tipizat ca signals: reconectare cu backoff și jitter, heartbeat, coadă de mesaje cât ești offline și siguranță la SSR.Timp real și offline
  • 15Actualizări optimiste și rollbackInterfață instantanee, dar onestă: snapshot și rollback, răspunsuri sosite în altă ordine, chei de idempotență.Timp real și offline
  • 15Offline-first: Service Worker și cacheService Worker-ul Angular: strategii de cache, anunțarea versiunilor noi și o coadă pentru modificările făcute offline.Timp real și offline
  • 16Web WorkersCalcule grele fără să blochezi interfața: un worker real în pagină, comunicare prin mesaje și când nu merită.UI & performanță avansată
  • 16Liste uriașe: virtual scroll100.000 de rânduri fără lag: CDK virtual scroll, itemSize, trackBy și alternativele (paginare, infinite scroll).UI & performanță avansată
  • 16Animații moderne & View Transitionsanimate.enter și animate.leave în locul @angular/animations (depreciat), tranziții CSS și View Transitions la navigare.UI & performanță avansată
  • 16Memory leaks: cum le găsești și le repariSubscripții, timere și listeneri care trăiesc mai mult decât componenta lor — cum îi vezi în DevTools și cum îi închizi.UI & performanță avansată
  • 17Securitate: XSS, sanitizare & CSPCe face Angular automat împotriva XSS, când devine periculos bypassSecurityTrust, plus CSP cu nonce, Trusted Types și XSRF.Securitate & globalizare
  • 17Autentificare: token-uri & refreshUn interceptor care atașează token-ul, reîmprospătează o singură dată la 401 și reia cererile din coadă — plus unde ții token-ul.Securitate & globalizare
  • 17OAuth 2.1 și OpenID Connect cu PKCEFluxul authorization code cu PKCE, pas cu pas: verifier și challenge, state și nonce, ID token-ul și de ce un BFF e mai sigur.Securitate & globalizare
  • 17Internaționalizare (i18n)$localize, atributul i18n, ICU plural/select, build per limbă vs. traduceri la runtime și formatarea după locale.Securitate & globalizare