La performance est la clé du succÚs d'une application Web. Par conséquent, les développeurs doivent savoir comment les fuites de mémoire se produisent et comment y faire face.Cette connaissance est particuliÚrement importante lorsque l'application avec laquelle le développeur traite atteint une certaine taille. Si vous ne faites pas assez attention aux fuites de mémoire, alors tout peut se retrouver avec le développeur entrant dans «l'équipe pour éliminer les fuites de mémoire» (je devais faire partie d'une telle équipe). Des fuites de mémoire peuvent se produire pour diverses raisons. Cependant, je pense que lorsque vous utilisez Angular, vous pouvez rencontrer un modÚle qui correspond à la cause la plus courante des fuites de mémoire. Il existe un moyen de gérer ces fuites de mémoire. Et la meilleure chose, bien sûr, n'est pas de lutter contre les problÚmes, mais de les éviter.
Qu'est-ce que la gestion de la mémoire?
JavaScript utilise un systÚme de gestion automatique de la mémoire. Le cycle de vie de la mémoire se compose généralement de trois étapes:- Allocation de la mémoire nécessaire.
- Travailler avec la mémoire allouée, effectuer des opérations de lecture et d'écriture.
- Libérer de la mémoire une fois qu'elle n'est plus nécessaire.
Sur MDN dit que la gestion automatique de la mĂ©moire - c'est une source potentielle de confusion. Cela peut donner aux dĂ©veloppeurs une fausse impression qu'ils n'ont pas Ă se soucier de la gestion de la mĂ©moire.Si vous ne vous souciez pas du tout de la gestion de la mĂ©moire, cela signifie qu'aprĂšs que votre application aura atteint une certaine taille, vous risquez de rencontrer une fuite de mĂ©moire.En gĂ©nĂ©ral, les fuites de mĂ©moire peuvent ĂȘtre considĂ©rĂ©es comme la mĂ©moire allouĂ©e Ă l'application, dont elle n'a plus besoin, mais qui n'est pas libĂ©rĂ©e. En d'autres termes, ce sont des objets qui n'ont pas pu subir d'opĂ©rations de rĂ©cupĂ©ration de place.Comment fonctionne la collecte des ordures?
Au cours de la procĂ©dure de collecte des ordures, ce qui est assez logique, tout ce qui peut ĂȘtre considĂ©rĂ© comme des «ordures» est nettoyĂ©. Le garbage collector nettoie la mĂ©moire dont l'application n'a plus besoin. Afin de dĂ©terminer les zones de mĂ©moire dont l'application a encore besoin, le garbage collector utilise l'algorithme «mark and sweep» (algorithme de balisage). Comme son nom l'indique, cet algorithme se compose de deux phases - la phase de marquage et la phase de balayage.â Phase de drapeau
Les objets et leurs liens sont prĂ©sentĂ©s sous la forme d'un arbre. La racine de l'arbre est, dans la figure suivante, un nĆud root
. En JavaScript, c'est un objet window
. Chaque objet a un drapeau spécial. Appelons ce drapeau marked
. Dans la phase de signalisation, tout d'abord, tous les indicateurs marked
sont définis sur une valeur false
.Au début, les drapeaux des objets marqués sont mis à faux,puis l'arborescence d'objets est parcourue. Tous les drapeaux d'marked
objets accessibles Ă partir du nĆudroot
sont définis surtrue
. Et les drapeaux de ces objets qui ne peuvent pas ĂȘtre atteints, restent dans la valeurfalse
.Un objet est considĂ©rĂ© comme inaccessible s'il ne peut pas ĂȘtre atteint Ă partir de l'objet racine.Les objets accessibles sont marquĂ©s comme Ă©tant marquĂ©s = true, les objets inaccessibles comme marquĂ©s = false.Par consĂ©quent, tous les indicateurs d'marked
objets inaccessibles restent dans la valeurfalse
. La mĂ©moire n'a pas encore Ă©tĂ© libĂ©rĂ©e, mais, une fois la phase de marquage terminĂ©e, tout est prĂȘt pour la phase de nettoyage.â Phase de nettoyage
La mémoire est effacée précisément à cette phase de l'algorithme. Ici, tous les objets inaccessibles (ceux dont l'indicateur marked
reste dans la valeur false
) sont dĂ©truits par le garbage collector.Arborescence d'objets aprĂšs la rĂ©cupĂ©ration de place. Tous les objets dont l'indicateur marquĂ© est dĂ©fini sur false sont dĂ©truits par le garbage collector. Legarbage collection est effectuĂ© pĂ©riodiquement pendant l'exĂ©cution du programme JavaScript. Au cours de cette procĂ©dure, la mĂ©moire est libĂ©rĂ©e et peut ĂȘtre libĂ©rĂ©e.Peut-ĂȘtre la question suivante se pose ici: "Si le garbage collector supprime tous les objets marquĂ©s comme inaccessibles - comment crĂ©er une fuite de mĂ©moire?".Le point ici est que l'objet ne sera pas traitĂ© par le garbage collector si l'application n'en a pas besoin, mais vous pouvez toujours l'atteindre Ă partir du nĆud racine de l'arborescence d'objets.L'algorithme ne peut pas savoir si l'application utilisera une partie de la mĂ©moire Ă laquelle elle peut accĂ©der ou non. Seul un programmeur possĂšde une telle connaissance.Fuites de mĂ©moire angulaire
Le plus souvent, des fuites de mémoire se produisent au fil du temps lorsqu'un composant est rendu à plusieurs reprises. Par exemple - via le routage ou suite à l'utilisation de la directive *ngIf
. Disons, dans une situation oĂč certains utilisateurs avancĂ©s travaillent avec l'application toute la journĂ©e sans mettre Ă jour la page de l'application dans le navigateur.Afin de reproduire ce scĂ©nario, nous allons crĂ©er une construction de deux composants. Ce seront les composants AppComponent
et SubComponent
.@Component({
selector: 'app-root',
template: `<app-sub *ngIf="hide"></app-sub>`
})
export class AppComponent {
hide = false;
constructor() {
setInterval(() => this.hide = !this.hide, 50);
}
}
Le modĂšle de composant AppComponent
utilise le composant app-sub
. La chose la plus intéressante ici est que notre composant utilise une fonction setInterval
qui commute l'indicateur hide
toutes les 50 ms. Il en résulte qu'un composant est restitué toutes les 50 ms app-sub
. Autrement dit, la création de nouvelles instances de la classe est effectuée SubComponent
. Ce code imite le comportement d'un utilisateur qui travaille toute la journée avec une application Web sans actualiser une page dans un navigateur.Nous avons, en SubComponent
, mis en Ćuvre diffĂ©rents scĂ©narios, dans l'utilisation desquels, au fil du temps, des changements dans la quantitĂ© de mĂ©moire utilisĂ©e par l'application commencent Ă apparaĂźtre. Notez que le composantAppComponent
reste toujours le mĂȘme. Dans chaque scĂ©nario, nous dĂ©couvrirons si nous avons affaire Ă une fuite de mĂ©moire en analysant la consommation de mĂ©moire du processus du navigateur.Si la consommation de mĂ©moire du processus augmente avec le temps, cela signifie que nous sommes confrontĂ©s Ă une fuite de mĂ©moire. Si un processus utilise une quantitĂ© de mĂ©moire plus ou moins constante, cela signifie soit qu'il n'y a pas de fuite de mĂ©moire, soit que la fuite, bien que prĂ©sente, ne se manifeste pas de maniĂšre assez Ă©vidente.â ScĂ©nario # 1: Ă©norme pour la boucle
Notre premier scĂ©nario est reprĂ©sentĂ© par une boucle qui s'exĂ©cute 100 000 fois. Dans la boucle, des valeurs alĂ©atoires sont ajoutĂ©es au tableau. N'oublions pas que le composant est restituĂ© toutes les 50 ms. Jetez un Ćil au code et rĂ©flĂ©chissez si nous avons créé une fuite de mĂ©moire ou non.@Component({
selector:'app-sub',
})
export class SubComponent {
arr = [];
constructor() {
for (let i = 0; i < 100000; ++i) {
this.arr.push(Math.random());
}
}
}
Bien que ce code ne doive pas ĂȘtre envoyĂ© en production, il ne crĂ©e pas de fuite de mĂ©moire. Ă savoir, la consommation de mĂ©moire ne dĂ©passe pas la plage limitĂ©e Ă une valeur de 15 Mo. Par consĂ©quent, il n'y a aucune fuite de mĂ©moire. Ci-dessous, nous expliquerons pourquoi il en est ainsi.â ScĂ©nario 2: abonnement Ă BehaviorSubject
Dans ce scénario, nous nous abonnons BehaviorSubject
et attribuons une valeur à une constante. Y a-t-il une fuite de mémoire dans ce code? Comme précédemment, n'oubliez pas que le composant est rendu toutes les 50 ms.@Component({
selector:'app-sub',
})
export class SubComponent {
subject = new BehaviorSubject(42);
constructor() {
this.subject.subscribe(value => {
const foo = value;
});
}
}
Ici, comme dans l'exemple prĂ©cĂ©dent, il n'y a pas de fuite de mĂ©moire.â ScĂ©nario 3: attribuer une valeur Ă un champ de classe dans un abonnement
Ici, presque le mĂȘme code est prĂ©sentĂ© comme dans l'exemple prĂ©cĂ©dent. La principale diffĂ©rence est que la valeur n'est pas attribuĂ©e Ă une constante, mais Ă un champ de classe. Et maintenant, pensez-vous qu'il y a une fuite dans le code?@Component({
selector:'app-sub',
})
export class SubComponent {
subject = new BehaviorSubject(42);
randomValue = 0;
constructor() {
this.subject.subscribe(value => {
this.randomValue = value;
});
}
}
Si vous croyez qu'il n'y a pas de fuite ici - vous avez absolument raison.Dans le scénario # 1, il n'y a pas d'abonnement. Dans les scénarios n ° 2 et 3, nous avons souscrit au flux de l'objet observé initialisé dans notre composante. Il semble que nous soyons en sécurité en souscrivant aux flux de composants.Mais que se passe-t-il si nous ajoutons un service à notre programme?Scénarios utilisant le service
Dans les scénarios suivants, nous allons réviser les exemples ci-dessus, mais cette fois, nous nous abonnerons au flux fourni par le service DummyService
. Voici le code de service.@Injectable({
providedIn: 'root'
})
export class DummyService {
some$ = new BehaviorSubject<number>(42);
}
Devant nous est un service simple. Il s'agit simplement d'un service qui fournit stream ( some$
) sous la forme d'un champ de classe publique.â ScĂ©nario 4: abonnement Ă un flux et attribution d'une valeur Ă une constante locale
Nous recrĂ©erons ici le mĂȘme schĂ©ma que celui dĂ©jĂ dĂ©crit prĂ©cĂ©demment. Mais cette fois, nous nous abonnons au flux some$
de DummyService
, et non au champ du composant.Y a-t-il une fuite de mémoire? Encore une fois, lorsque vous répondez à cette question, n'oubliez pas que le composant est utilisé AppComponent
et rendu plusieurs fois.@Component({
selector:'app-sub',
})
export class SubComponent {
constructor(private dummyService: DummyService) {
this.dummyService.some$.subscribe(value => {
const foo = value;
});
}
}
Et maintenant, nous avons finalement créé une fuite de mémoire. Mais c'est une petite fuite. Par "petite fuite", j'entends celle qui, avec le temps, entraßne une lente augmentation de la quantité de mémoire consommée. Cette augmentation est à peine perceptible, mais une inspection rapide de l'instantané du tas a montré la présence de nombreuses instances non supprimées Subscriber
.â ScĂ©nario 5: abonnement Ă un service et attribution d'une valeur Ă un champ de classe
Ici, nous nous abonnons Ă nouveau Ă dummyService
. Mais cette fois, nous attribuons la valeur résultante au champ de classe, et non une constante locale.@Component({
selector:'app-sub',
})
export class SubComponent {
randomValue = 0;
constructor(private dummyService: DummyService) {
this.dummyService.some$.subscribe(value => {
this.randomValue = value;
});
}
}
Et ici, nous avons finalement créé une fuite de mémoire importante. La consommation de mémoire rapidement, en moins d'une minute, a dépassé 1 Go. Parlons pourquoi il en est ainsi.HenQuand une fuite de mémoire s'est-elle produite?
Vous avez peut-ĂȘtre remarquĂ© que dans les trois premiers scĂ©narios, nous n'avons pas pu crĂ©er de fuite de mĂ©moire. Ces trois scĂ©narios ont quelque chose en commun: tous les liens sont locaux vers le composant.Lorsque nous nous abonnons Ă un objet observable, il stocke une liste d'abonnĂ©s. Notre rappel est Ă©galement sur cette liste, et le rappel peut se rĂ©fĂ©rer Ă notre composant.Aucune fuite de mĂ©moireLorsqu'un composant est dĂ©truit, c'est-Ă -dire lorsque Angular n'a plus de lien vers lui, ce qui signifie que le composant ne peut pas ĂȘtre atteint depuis le nĆud racine, l'objet observĂ© et sa liste d'abonnĂ©s ne peuvent pas non plus ĂȘtre atteints depuis le nĆud racine. Par consĂ©quent, l'objet composant entier est rĂ©cupĂ©rĂ©.Tant que nous sommes abonnĂ©s Ă un objet observable, dont les liens ne sont qu'Ă l'intĂ©rieur du composant, aucun problĂšme ne se pose. Mais lorsque le service entre en jeu, la situation change.Fuite de mĂ©moireDĂšs que nous nous sommes abonnĂ©s Ă un objet observable fourni par un service ou une autre classe, nous avons créé une fuite de mĂ©moire. Cela est dĂ» Ă l'objet observĂ©, en raison de sa liste d'abonnĂ©s. Pour cette raison, le rappel, et donc le composant, sont accessibles Ă partir du nĆud racine, bien qu'Angular n'ait pas de rĂ©fĂ©rence directe au composant. Par consĂ©quent, le garbage collector ne touche pas l'objet correspondant.Je vais clarifier: vous pouvez utiliser de telles constructions, mais vous devez travailler avec elles correctement, et pas comme nous le faisons.Bon travail d'abonnement
Afin d'éviter une fuite de mémoire, il est important de se désinscrire correctement de l'objet observé, en le faisant lorsque l'abonnement n'est plus nécessaire. Par exemple, lorsqu'un composant est détruit. Il existe de nombreuses façons de se désinscrire d'un objet observé.L'expérience de conseiller les propriétaires de grands projets d'entreprise indique que dans cette situation, il est préférable d'utiliser l'entité destroy$
créée par l'équipe new Subject<void>()
en combinaison avec l'opérateur takeUntil
.@Component({
selector:'app-sub',
})
export class SubComponent implements OnDestroy {
private destroy$: Subject<void> = new Subject<void>();
randomNumber = 0;
constructor(private dummyService: DummyService) {
dummyService.some$.pipe(
takeUntil(this.destroy$)
).subscribe(value => this.randomNumber = value);
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}
Ici, nous nous désinscrivons de l'abonnement en utilisant l' destroy$
opérateur et takeUntil
aprÚs la destruction du composant.Nous avons implémenté un hook de cycle de vie dans le composant ngOnDestroy
. Chaque fois qu'un composant est détruit, nous appelons les destroy$
méthodes next
et complete
.L'appel est complete
trĂšs important car cet appel efface l'abonnement destroy$
.Ensuite, nous utilisons l'opérateur takeUntil
et lui transmettons notre flux destroy$
. Cela garantit que l'abonnement est effacé (c'est-à -dire que nous nous sommes désabonnés de l'abonnement) aprÚs la destruction du composant.Comment ne pas oublier d'effacer les abonnements?
Il est facile d'oublier d'ajouter le composant destroy$
et d'oublier d'appeler next
, ainsi que complete
dans le cycle de vie du crochet ngOnDestroy
. MĂȘme si j'ai enseignĂ© cela aux Ă©quipes travaillant sur des projets, je l'ai souvent oubliĂ© moi-mĂȘme.Heureusement, il existe une merveilleuse rĂšgle de linter, qui fait partie d'un ensemble de rĂšgles qui vous permet d'assurer une bonne dĂ©sinscription des abonnements. Vous pouvez dĂ©finir un ensemble de rĂšgles comme celui-ci:npm install @angular-extensions/lint-rules --save-dev
Ensuite, il doit ĂȘtre connectĂ© Ă tslint.json
:{
"extends": [
"tslint:recommended",
"@angular-extensions/lint-rules"
]
}
Je vous recommande fortement d'utiliser cet ensemble de rÚgles dans vos projets. Cela vous fera économiser de nombreuses heures de débogage pour trouver les sources de fuites de mémoire.Sommaire
Dans Angular, il est trĂšs facile de crĂ©er une situation entraĂźnant des fuites de mĂ©moire. MĂȘme de petits changements de code dans des endroits qui, apparemment, ne devraient pas ĂȘtre liĂ©s Ă des fuites de mĂ©moire, peuvent entraĂźner de graves consĂ©quences nĂ©fastes.La meilleure façon d'Ă©viter les fuites de mĂ©moire est de gĂ©rer correctement vos abonnements. Malheureusement, l'opĂ©ration de nettoyage des abonnements nĂ©cessite une grande prĂ©cision de la part du dĂ©veloppeur. C'est facile Ă oublier. Par consĂ©quent, il est recommandĂ© d'appliquer des rĂšgles @angular-extensions/lint-rules
qui vous aident à organiser le bon travail avec vos abonnements.Voici le référentiel avec le code sous-jacent à ce matériel.Avez-vous rencontré des fuites de mémoire dans des applications angulaires?