Cours JavaScript et TypeScript · Module 10: TypeScript en pratique
Gestion de la mémoire - Garbage Collection, Memory Leaks
Dans cette leçon6
Imagine un enclos d'où personne n'évacue les restes de nourriture. Pendant quelques jours, rien ne se voit, puis au bout d'un mois, le T-Rex n'a plus la place de poser une patte. C'est ainsi que fonctionne une fuite de mémoire (memory leak) : l'application conserve des données dont elle n'a plus besoin, et ralentit d'heure en heure. La gestion de la mémoire est l'un des aspects les plus importants des performances. Même si JavaScript dispose d'un garbage collector automatique, tu dois comprendre le fonctionnement de la mémoire pour éviter les fuites.
Comment fonctionne la mémoire en JavaScript
Memory lifecycle
Chaque valeur passe par trois étapes : l'allocation, l'utilisation et la libération. Tu vois les deux premières dans le code, la troisième est effectuée à ta place par le garbage collector (GC) :
1// Cycle de vie de la mémoire en JavaScript
2const memoryLifecycle = {
3 // 1. Allocation de la mémoire - automatique
4 allocation: () => {
5 const obj = { name: 'John', age: 30 }; // Allocation dans le heap
6 const arr = [1, 2, 3, 4, 5]; // Allocation dans le heap
7 const num = 42; // Allocation dans la stack (primitive)
8
9 return { obj, arr, num };
10 },
11
12 // 2. Utilisation de la mémoire - lecture/écriture
13 usage: (data) => {
14 data.obj.name = 'Jane'; // Utilisation de la mémoire allouée
15 data.arr.push(6);
16
17 console.log(data.obj, data.arr);
18 },
19
20 // 3. Libération de la mémoire - automatique, par le GC
21 release: () => {
22 // Quand les variables sortent du scope, elles deviennent candidates au GC
23 // Le GC se lancera automatiquement au moment opportun
24 }
25};
26
27// Démonstration du cycle
28function demonstrateMemoryLifecycle() {
29 const data = memoryLifecycle.allocation(); // Allocation
30 memoryLifecycle.usage(data); // Utilisation
31 // Après la fin de la fonction, 'data' devient candidat au GC
32}
33
34demonstrateMemoryLifecycle();Tu n'appelles aucun free(). Quand data n'est plus accessible après la fin de la fonction, l'objet devient candidat au nettoyage, et c'est le GC qui en choisira le moment.
Stack vs Heap
On dit souvent que les primitives vivent sur la pile (stack) et les objets sur le tas (heap) :
1// Stack - stocke les primitives et les références
2function stackExample() {
3 const a = 5; // Stack
4 const b = 'hello'; // Stack (référence vers une chaîne dans le heap)
5 const c = true; // Stack
6
7 console.log('Stack variables:', a, b, c);
8} // Après la fin de la fonction, tout ce qui était dans la stack est supprimé
9
10// Heap - stocke les objets et les tableaux
11function heapExample() {
12 const obj = { x: 1, y: 2 }; // Objet dans le heap, référence dans la stack
13 const arr = [1, 2, 3]; // Tableau dans le heap, référence dans la stack
14
15 // Modification de l'objet dans le heap
16 obj.z = 3;
17 arr.push(4);
18
19 return { obj, arr }; // On renvoie les références
20} // Les références de la stack sont supprimées, mais les objets du heap restent
21
22// Memory allocation visualization
23class MemoryVisualizer {
24 constructor() {
25 this.allocations = [];
26 this.currentId = 0;
27 }
28
29 allocate(type, size, description) {
30 const allocation = {
31 id: this.currentId++,
32 type: type, // 'stack' | 'heap'
33 size: size,
34 description: description,
35 timestamp: Date.now(),
36 freed: false
37 };
38
39 this.allocations.push(allocation);
40 console.log(`Allocated ${type}: ${description} (${size} bytes)`);
41
42 return allocation.id;
43 }
44
45 free(id) {
46 const allocation = this.allocations.find(a => a.id === id);
47 if (allocation) {
48 allocation.freed = true;
49 allocation.freedAt = Date.now();
50 console.log(`Freed: ${allocation.description}`);
51 }
52 }
53
54 getMemoryStats() {
55 const active = this.allocations.filter(a => !a.freed);
56 const freed = this.allocations.filter(a => a.freed);
57
58 return {
59 totalAllocations: this.allocations.length,
60 activeAllocations: active.length,
61 freedAllocations: freed.length,
62 totalActiveSize: active.reduce((sum, a) => sum + a.size, 0),
63 memoryLeaks: active.filter(a => Date.now() - a.timestamp > 30000) // > 30s
64 };
65 }
66}
67
68const memoryViz = new MemoryVisualizer();
69
70// Exemple d'utilisation
71function demonstrateMemoryAllocation() {
72 // Stack allocations
73 const stackId1 = memoryViz.allocate('stack', 8, 'number variable');
74 const stackId2 = memoryViz.allocate('stack', 8, 'string reference');
75
76 // Heap allocations
77 const heapId1 = memoryViz.allocate('heap', 64, 'object {name, age, city}');
78 const heapId2 = memoryViz.allocate('heap', 32, 'array [1,2,3,4,5]');
79
80 // Simulation de la libération de mémoire
81 setTimeout(() => {
82 memoryViz.free(stackId1);
83 memoryViz.free(stackId2);
84 console.log('Memory stats:', memoryViz.getMemoryStats());
85 }, 1000);
86
87 setTimeout(() => {
88 memoryViz.free(heapId1);
89 // heapId2 n'est pas libéré - simulation d'un memory leak
90 console.log('Final memory stats:', memoryViz.getMemoryStats());
91 }, 2000);
92}C'est un modèle simplifié. La spécification ECMAScript ne précise pas où les valeurs se trouvent en mémoire, c'est donc le moteur qui décide : par exemple, les chaînes et les variables capturées par des closures atterrissent sur le tas. MemoryVisualizer ne fait que simuler des allocations, mais il montre bien ce qu'est une fuite : une entrée que personne ne libérera jamais.
Garbage Collection en JavaScript
Les types d'algorithmes de GC
Compare deux stratégies. Le comptage de références (reference counting) supprime un objet quand plus rien ne pointe vers lui. Le mark-and-sweep part des racines (roots), comme les variables globales et la pile d'appels, marque tout ce qui est accessible depuis elles et supprime le reste :
1// 1. Reference Counting (ancien algorithme)
2class ReferenceCountingDemo {
3 constructor() {
4 this.objects = new Map();
5 }
6
7 createObject(id, data) {
8 const obj = {
9 id: id,
10 data: data,
11 refCount: 1,
12 references: []
13 };
14
15 this.objects.set(id, obj);
16 console.log(`Created object ${id} with ref count: 1`);
17 return obj;
18 }
19
20 addReference(fromId, toId) {
21 const fromObj = this.objects.get(fromId);
22 const toObj = this.objects.get(toId);
23
24 if (fromObj && toObj) {
25 fromObj.references.push(toId);
26 toObj.refCount++;
27 console.log(`Object ${toId} ref count increased to: ${toObj.refCount}`);
28 }
29 }
30
31 removeReference(fromId, toId) {
32 const fromObj = this.objects.get(fromId);
33 const toObj = this.objects.get(toId);
34
35 if (fromObj && toObj) {
36 const index = fromObj.references.indexOf(toId);
37 if (index > -1) {
38 fromObj.references.splice(index, 1);
39 toObj.refCount--;
40 console.log(`Object ${toId} ref count decreased to: ${toObj.refCount}`);
41
42 // Auto GC when ref count reaches 0
43 if (toObj.refCount === 0) {
44 this.collectObject(toId);
45 }
46 }
47 }
48 }
49
50 collectObject(id) {
51 const obj = this.objects.get(id);
52 if (obj) {
53 // Recursively decrease reference counts
54 obj.references.forEach(refId => {
55 this.removeReference(id, refId);
56 });
57
58 this.objects.delete(id);
59 console.log(`Collected object ${id}`);
60 }
61 }
62
63 // Problem: Circular references
64 createCircularReference() {
65 const obj1 = this.createObject('circular1', 'First object');
66 const obj2 = this.createObject('circular2', 'Second object');
67
68 // Références mutuelles - memory leak avec le reference counting !
69 this.addReference('circular1', 'circular2');
70 this.addReference('circular2', 'circular1');
71
72 console.log('Created circular reference - will never be collected!');
73
74 return { obj1, obj2 };
75 }
76}
77
78// 2. Mark and Sweep (algorithme moderne)
79class MarkAndSweepDemo {
80 constructor() {
81 this.objects = new Map();
82 this.roots = new Set(); // Global variables, stack references
83 }
84
85 createObject(id, data) {
86 const obj = {
87 id: id,
88 data: data,
89 references: [],
90 marked: false
91 };
92
93 this.objects.set(id, obj);
94 return obj;
95 }
96
97 addToRoots(id) {
98 this.roots.add(id);
99 console.log(`Added ${id} to GC roots`);
100 }
101
102 removeFromRoots(id) {
103 this.roots.delete(id);
104 console.log(`Removed ${id} from GC roots`);
105 }
106
107 addReference(fromId, toId) {
108 const fromObj = this.objects.get(fromId);
109 if (fromObj && !fromObj.references.includes(toId)) {
110 fromObj.references.push(toId);
111 }
112 }
113
114 // Mark phase - marque tous les objets accessibles
115 markPhase() {
116 console.log('Starting mark phase...');
117
118 // Effacer les marquages précédents
119 this.objects.forEach(obj => obj.marked = false);
120
121 // Marquer tous les objets accessibles depuis les roots
122 const visited = new Set();
123
124 const markRecursive = (id) => {
125 if (visited.has(id)) return;
126 visited.add(id);
127
128 const obj = this.objects.get(id);
129 if (obj) {
130 obj.marked = true;
131 console.log(` Marked object ${id}`);
132
133 // Marquer récursivement les références
134 obj.references.forEach(refId => markRecursive(refId));
135 }
136 };
137
138 // Commencer par toutes les roots
139 this.roots.forEach(rootId => markRecursive(rootId));
140 }
141
142 // Sweep phase - supprime les objets non marqués
143 sweepPhase() {
144 console.log('Starting sweep phase...');
145
146 const toDelete = [];
147
148 this.objects.forEach((obj, id) => {
149 if (!obj.marked) {
150 toDelete.push(id);
151 }
152 });
153
154 toDelete.forEach(id => {
155 console.log(`Collecting object ${id}`);
156 this.objects.delete(id);
157 });
158
159 console.log(`Collected ${toDelete.length} objects`);
160 }
161
162 // Cycle de GC complet
163 runGarbageCollection() {
164 console.log('Running garbage collection...');
165 this.markPhase();
166 this.sweepPhase();
167 console.log(`Active objects: ${this.objects.size}`);
168 }
169
170 // Démonstration de la résolution des circular references
171 demonstrateCircularReferences() {
172 // Création d'objets avec des circular references
173 const obj1 = this.createObject('mark1', 'Object 1');
174 const obj2 = this.createObject('mark2', 'Object 2');
175 const obj3 = this.createObject('mark3', 'Object 3');
176
177 // Circular references
178 this.addReference('mark1', 'mark2');
179 this.addReference('mark2', 'mark3');
180 this.addReference('mark3', 'mark1'); // Circular!
181
182 // Seul obj1 est dans les roots
183 this.addToRoots('mark1');
184
185 console.log('Before GC - objects with circular references');
186 this.runGarbageCollection(); // Tous seront marqués comme accessibles
187
188 // On les retire des roots
189 this.removeFromRoots('mark1');
190
191 console.log('After removing from roots');
192 this.runGarbageCollection(); // Tous seront collectés malgré les circular references !
193 }
194}Deux objets qui pointent l'un vers l'autre tromperont le comptage de références, mais pas le mark-and-sweep : quand aucune racine ne mène à eux, ils disparaissent tous les deux. C'est pourquoi les moteurs modernes utilisent le mark-and-sweep, et un cycle de références n'est plus, à lui seul, une fuite. Après les phases mark et sweep, le moteur peut encore compacter la mémoire (compact) pour boucher les trous.
Generational Garbage Collection
La plupart des objets vivent peu de temps, donc les moteurs divisent le tas en générations. Ils nettoient la jeune génération souvent et rapidement, et les objets qui survivent à plusieurs cycles passent dans l'ancienne :
1// Les moteurs JS modernes utilisent un generational GC
2class GenerationalGCDemo {
3 constructor() {
4 // Différentes générations d'objets
5 this.youngGeneration = new Map(); // Nouveaux objets
6 this.oldGeneration = new Map(); // Anciens objets
7 this.permanentGeneration = new Map(); // Très anciens objets
8
9 this.gcCycles = 0;
10 this.promotionThreshold = 3; // Nombre de cycles de GC à survivre pour être promu
11 }
12
13 allocateObject(id, data, size = 64) {
14 const obj = {
15 id: id,
16 data: data,
17 size: size,
18 generation: 'young',
19 gcSurvived: 0,
20 allocatedAt: Date.now(),
21 lastAccessed: Date.now()
22 };
23
24 this.youngGeneration.set(id, obj);
25 console.log(`Allocated ${id} in young generation`);
26 return obj;
27 }
28
29 accessObject(id) {
30 let obj = this.youngGeneration.get(id) ||
31 this.oldGeneration.get(id) ||
32 this.permanentGeneration.get(id);
33
34 if (obj) {
35 obj.lastAccessed = Date.now();
36 console.log(`Accessed object ${id}`);
37 }
38
39 return obj;
40 }
41
42 // Minor GC - uniquement la young generation
43 runMinorGC() {
44 console.log('Running Minor GC (young generation)...');
45 this.gcCycles++;
46
47 const survivors = [];
48 const toPromote = [];
49
50 this.youngGeneration.forEach((obj, id) => {
51 // Simulation : les objets utilisés au cours des 5 dernières secondes survivent
52 const timeSinceLastAccess = Date.now() - obj.lastAccessed;
53
54 if (timeSinceLastAccess < 5000) {
55 obj.gcSurvived++;
56 survivors.push({ id, obj });
57
58 // Promotion vers l'old generation
59 if (obj.gcSurvived >= this.promotionThreshold) {
60 toPromote.push({ id, obj });
61 }
62
63 console.log(`Object ${id} survived (count: ${obj.gcSurvived})`);
64 } else {
65 console.log(`Collected young object ${id}`);
66 }
67 });
68
69 // Vider la young generation
70 this.youngGeneration.clear();
71
72 // Restaurer les survivants
73 survivors.forEach(({ id, obj }) => {
74 if (!toPromote.find(p => p.id === id)) {
75 this.youngGeneration.set(id, obj);
76 }
77 });
78
79 // Promouvoir vers l'old generation
80 toPromote.forEach(({ id, obj }) => {
81 obj.generation = 'old';
82 this.oldGeneration.set(id, obj);
83 console.log(`Promoted ${id} to old generation`);
84 });
85
86 console.log(`Minor GC completed. Young: ${this.youngGeneration.size}, Old: ${this.oldGeneration.size}`);
87 }
88
89 // Major GC - toutes les générations
90 runMajorGC() {
91 console.log('Running Major GC (all generations)...');
92
93 const collectFromGeneration = (generation, name) => {
94 const survivors = new Map();
95 let collected = 0;
96
97 generation.forEach((obj, id) => {
98 const timeSinceLastAccess = Date.now() - obj.lastAccessed;
99
100 // L'old generation a une durée de vie plus longue
101 const threshold = name === 'old' ? 10000 : 5000;
102
103 if (timeSinceLastAccess < threshold) {
104 survivors.set(id, obj);
105 } else {
106 collected++;
107 console.log(`Collected ${name} object ${id}`);
108 }
109 });
110
111 generation.clear();
112 survivors.forEach((obj, id) => generation.set(id, obj));
113
114 return collected;
115 };
116
117 const youngCollected = collectFromGeneration(this.youngGeneration, 'young');
118 const oldCollected = collectFromGeneration(this.oldGeneration, 'old');
119
120 console.log(`Major GC completed. Collected ${youngCollected + oldCollected} objects`);
121 console.log(`Remaining - Young: ${this.youngGeneration.size}, Old: ${this.oldGeneration.size}`);
122 }
123
124 // GC automatique basé sur des heuristiques
125 autoGC() {
126 const youngSize = this.youngGeneration.size;
127 const oldSize = this.oldGeneration.size;
128
129 // Déclencheurs du GC
130 if (youngSize > 100) { // Beaucoup de jeunes objets
131 this.runMinorGC();
132 }
133
134 if (oldSize > 50 || this.gcCycles % 10 === 0) { // Beaucoup d'anciens objets ou tous les 10 cycles
135 this.runMajorGC();
136 }
137 }
138
139 getGenerationStats() {
140 return {
141 young: this.youngGeneration.size,
142 old: this.oldGeneration.size,
143 permanent: this.permanentGeneration.size,
144 totalGCCycles: this.gcCycles
145 };
146 }
147}
148
149// Démonstration du generational GC
150const genGC = new GenerationalGCDemo();
151
152// Simulation d'allocation patterns
153function simulateAllocationPattern() {
154 // Création de nombreux objets éphémères
155 for (let i = 0; i < 50; i++) {
156 genGC.allocateObject(`temp_${i}`, `Temporary object ${i}`);
157 }
158
159 // Création de quelques objets à longue durée de vie
160 for (let i = 0; i < 5; i++) {
161 const longLived = genGC.allocateObject(`long_${i}`, `Long-lived object ${i}`);
162
163 // Simulation d'accès réguliers
164 setInterval(() => {
165 genGC.accessObject(`long_${i}`);
166 }, 1000);
167 }
168
169 // GC automatique toutes les deux secondes
170 setInterval(() => {
171 genGC.autoGC();
172 console.log('Generation stats:', genGC.getGenerationStats());
173 }, 2000);
174}La simulation simplifie un point : un vrai GC ne vérifie pas quand tu as utilisé un objet pour la dernière fois, mais s'il est accessible. V8 a une jeune et une ancienne génération, et la génération permanent est une notion héritée de l'ancien Java.
Les types de memory leaks
Variables globales, listeners, timers, closures et DOM
Une fuite, c'est un objet encore accessible alors que tu n'en as plus besoin. Voici cinq sources typiques, accompagnées de leurs versions correctes :
1// Problème : des variables globales incontrôlées
2class GlobalVariableLeaks {
3 static demonstrateLeaks() {
4 // BAD: des variables globales accidentelles
5 // Attention : le code d'une classe s'exécute toujours en mode strict, donc ici les deux lignes lèvent une erreur.
6 // Cette fuite se produit dans un script ordinaire sans 'use strict'.
7 function accidentalGlobal() {
8 // Pas de 'var', 'let', 'const' - crée une variable globale !
9 leakedVariable = 'This will leak!';
10
11 // Globale aussi via 'this' en non-strict mode
12 this.anotherLeak = { data: new Array(1000).fill('leak') };
13 }
14
15 accidentalGlobal();
16
17 // Ces variables resteront en mémoire jusqu'à la fin de l'application
18 console.log('Leaked variables:', window.leakedVariable, window.anotherLeak);
19 }
20
21 static demonstrateSolutions() {
22 // GOOD: utilise le strict mode
23 'use strict';
24
25 function properFunction() {
26 // ReferenceError au lieu d'une variable globale
27 // notDeclared = 'This will throw error in strict mode';
28
29 let properVariable = 'This is properly scoped';
30 const anotherProper = { data: 'safe' };
31
32 return { properVariable, anotherProper };
33 }
34
35 // GOOD: le pattern Module
36 const safeModule = (function() {
37 let privateData = [];
38
39 return {
40 addData: function(item) {
41 privateData.push(item);
42 },
43
44 getData: function() {
45 return privateData.slice(); // Return copy
46 },
47
48 clear: function() {
49 privateData = [];
50 }
51 };
52 })();
53
54 return { safeModule };
55 }
56}
57
58// 2. Event Listeners
59class EventListenerLeaks {
60 constructor() {
61 this.data = new Array(10000).fill('memory consuming data');
62 this.handlers = new Map();
63 }
64
65 // BAD: des event listeners sans cleanup
66 addLeakyListeners() {
67 const button = document.getElementById('leaky-button');
68
69 // Problème : le listener référence un objet qui peut contenir beaucoup de données
70 const handler = () => {
71 console.log('Clicked!', this.data.length);
72 };
73
74 button?.addEventListener('click', handler);
75
76 // Quand le composant est supprimé, le listener reste !
77 // L'objet entier ne peut pas être collecté par le GC
78 }
79
80 // GOOD: Proper cleanup
81 addProperListeners() {
82 const button = document.getElementById('proper-button');
83
84 const handler = (event) => {
85 console.log('Proper click!', event.target.id);
86 };
87
88 this.handlers.set('button-click', handler);
89 button?.addEventListener('click', handler);
90 }
91
92 cleanup() {
93 // Supprime toujours les event listeners pendant le cleanup
94 this.handlers.forEach((handler, key) => {
95 if (key === 'button-click') {
96 const button = document.getElementById('proper-button');
97 button?.removeEventListener('click', handler);
98 }
99 });
100
101 this.handlers.clear();
102 console.log('Event listeners cleaned up');
103 }
104
105 // Modern approach: AbortController
106 addModernListeners() {
107 const controller = new AbortController();
108 const signal = controller.signal;
109
110 document.addEventListener('click', (event) => {
111 console.log('Modern click!', event.target);
112 }, { signal });
113
114 // Cleanup de tous les listeners d'un coup
115 setTimeout(() => {
116 controller.abort();
117 console.log('All listeners aborted');
118 }, 10000);
119 }
120}
121
122// 3. Timers et Intervals
123class TimerLeaks {
124 constructor() {
125 this.data = new Array(100000).fill('timer data');
126 this.timers = new Set();
127 }
128
129 // BAD: des timers sans cleanup
130 createLeakyTimers() {
131 // Problème : setInterval référence la classe entière
132 const intervalId = setInterval(() => {
133 console.log('Leaky timer tick', this.data.length);
134 }, 1000);
135
136 // Le timer n'est jamais nettoyé !
137 // L'objet ne peut pas être collecté par le GC
138
139 return intervalId;
140 }
141
142 // GOOD: Tracked timers
143 createTrackedTimers() {
144 const intervalId = setInterval(() => {
145 console.log('Tracked timer tick');
146 }, 1000);
147
148 this.timers.add(intervalId);
149
150 // Auto cleanup au bout d'un certain temps
151 const timeoutId = setTimeout(() => {
152 this.clearTimer(intervalId);
153 }, 10000);
154
155 this.timers.add(timeoutId);
156
157 return intervalId;
158 }
159
160 clearTimer(timerId) {
161 clearInterval(timerId);
162 clearTimeout(timerId);
163 this.timers.delete(timerId);
164 console.log(`Timer ${timerId} cleared`);
165 }
166
167 cleanup() {
168 this.timers.forEach(timerId => {
169 clearInterval(timerId);
170 clearTimeout(timerId);
171 });
172
173 this.timers.clear();
174 console.log('All timers cleaned up');
175 }
176
177 // Approche moderne : AbortController pour les opérations async
178 createAbortableTimer() {
179 const controller = new AbortController();
180
181 const timer = {
182 start: () => {
183 const interval = setInterval(() => {
184 if (controller.signal.aborted) {
185 clearInterval(interval);
186 return;
187 }
188
189 console.log('Abortable timer tick');
190 }, 1000);
191
192 return interval;
193 },
194
195 stop: () => {
196 controller.abort();
197 }
198 };
199
200 return timer;
201 }
202}
203
204// 4. Closures
205class ClosureLeaks {
206 static demonstrateLeaks() {
207 function createLeakyClosures() {
208 const largeData = new Array(1000000).fill('leak data');
209
210 // Problème : la closure garde une référence vers tout le scope
211 return {
212 // Même si on n'utilise qu'une petite partie des données
213 getSmallPart: function() {
214 return largeData.slice(0, 10);
215 },
216
217 // Cette fonction garde elle aussi tout largeData en mémoire
218 getLength: function() {
219 return largeData.length;
220 }
221 };
222 }
223
224 const leaky = createLeakyClosures();
225 console.log('Small part:', leaky.getSmallPart());
226
227 // largeData reste en mémoire alors qu'on n'a besoin que de sa longueur !
228 return leaky;
229 }
230
231 static demonstrateSolutions() {
232 function createOptimizedClosures() {
233 const largeData = new Array(1000000).fill('optimized data');
234
235 // GOOD: extrais uniquement les données nécessaires
236 const smallPart = largeData.slice(0, 10);
237 const dataLength = largeData.length;
238
239 // largeData peut être collecté par le GC
240
241 return {
242 getSmallPart: function() {
243 return smallPart;
244 },
245
246 getLength: function() {
247 return dataLength;
248 }
249 };
250 }
251
252 // GOOD: Null references when done
253 function createCleanClosures() {
254 let data = new Array(100000).fill('clean data');
255
256 const api = {
257 process: function() {
258 const result = data.map(item => item.toUpperCase());
259 data = null; // Explicit cleanup!
260 return result;
261 }
262 };
263
264 return api;
265 }
266
267 return { createOptimizedClosures, createCleanClosures };
268 }
269}
270
271// 5. DOM References
272class DOMReferenceLeaks {
273 constructor() {
274 this.domReferences = new Map();
275 this.observers = [];
276 }
277
278 // BAD: Holding DOM references
279 createDOMLeaks() {
280 // Problème : des références vers des éléments DOM supprimés
281 const elements = document.querySelectorAll('.dynamic-content');
282
283 elements.forEach((element, index) => {
284 this.domReferences.set(`element_${index}`, {
285 node: element, // Direct reference!
286 data: new Array(1000).fill(`data_${index}`)
287 });
288 });
289
290 // Même si les éléments sont retirés du DOM,
291 // ils resteront en mémoire à cause de ces références !
292 }
293
294 // GOOD: WeakMap pour les références DOM
295 createSafeDOMReferences() {
296 const elementData = new WeakMap();
297
298 const elements = document.querySelectorAll('.safe-content');
299
300 elements.forEach((element, index) => {
301 // WeakMap permet au GC de collecter l'élément quand il est retiré du DOM
302 elementData.set(element, {
303 id: index,
304 data: new Array(1000).fill(`safe_data_${index}`)
305 });
306 });
307
308 return elementData;
309 }
310
311 // Cleanup correct des observers DOM
312 setupDOMObserver() {
313 const observer = new MutationObserver((mutations) => {
314 mutations.forEach(mutation => {
315 console.log('DOM changed:', mutation.type);
316 });
317 });
318
319 observer.observe(document.body, {
320 childList: true,
321 subtree: true
322 });
323
324 this.observers.push(observer);
325
326 // Auto cleanup
327 setTimeout(() => {
328 this.cleanupObservers();
329 }, 30000);
330 }
331
332 cleanupObservers() {
333 this.observers.forEach(observer => {
334 observer.disconnect();
335 });
336
337 this.observers = [];
338 console.log('DOM observers cleaned up');
339 }
340
341 cleanup() {
342 this.domReferences.clear();
343 this.cleanupObservers();
344 }
345}La cause la plus fréquente, ce sont les event listeners et les timers oubliés. Un listener posé sur un objet qui vit plus longtemps que le composant, par exemple document, maintient en vie tout ce que sa closure atteint. AbortController supprime de nombreux listeners avec un seul abort(), et WeakMap n'empêche pas le nettoyage d'un élément du DOM. Dans l'exemple des variables globales, n'oublie pas que les classes fonctionnent en mode strict, donc une affectation à une variable non déclarée lèvera une ReferenceError.
Memory Leak Detection et Monitoring
Performance API pour le Memory Monitoring
Une fuite se trahit par une hausse constante de la consommation de mémoire. Le moniteur enregistre la taille du tas toutes les quelques secondes et avertit quand elle grimpe sans interruption :
1// Memory monitoring utilities
2class MemoryMonitor {
3 constructor() {
4 this.measurements = [];
5 this.isMonitoring = false;
6 this.alertThreshold = 50 * 1024 * 1024; // 50MB
7 }
8
9 startMonitoring(interval = 5000) {
10 if (this.isMonitoring) return;
11
12 this.isMonitoring = true;
13 console.log('Started memory monitoring');
14
15 const monitor = () => {
16 if (!this.isMonitoring) return;
17
18 const memInfo = this.getMemoryInfo();
19 this.measurements.push({
20 ...memInfo,
21 timestamp: Date.now()
22 });
23
24 this.checkMemoryAlerts(memInfo);
25 this.trimMeasurements();
26
27 setTimeout(monitor, interval);
28 };
29
30 monitor();
31 }
32
33 stopMonitoring() {
34 this.isMonitoring = false;
35 console.log('⏹Stopped memory monitoring');
36 }
37
38 getMemoryInfo() {
39 if (performance.memory) {
40 return {
41 usedJSHeapSize: performance.memory.usedJSHeapSize,
42 totalJSHeapSize: performance.memory.totalJSHeapSize,
43 jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
44 usedPercent: (performance.memory.usedJSHeapSize / performance.memory.jsHeapSizeLimit) * 100
45 };
46 }
47
48 return {
49 usedJSHeapSize: 0,
50 totalJSHeapSize: 0,
51 jsHeapSizeLimit: 0,
52 usedPercent: 0
53 };
54 }
55
56 checkMemoryAlerts(memInfo) {
57 if (memInfo.usedJSHeapSize > this.alertThreshold) {
58 console.warn('High memory usage detected:', {
59 used: this.formatBytes(memInfo.usedJSHeapSize),
60 percent: memInfo.usedPercent.toFixed(2) + '%'
61 });
62
63 this.triggerMemoryCleanup();
64 }
65
66 // Check for memory leaks (constant growth)
67 if (this.measurements.length > 10) {
68 const recent = this.measurements.slice(-10);
69 const growth = recent[recent.length - 1].usedJSHeapSize - recent[0].usedJSHeapSize;
70 const timeSpan = recent[recent.length - 1].timestamp - recent[0].timestamp;
71 const growthRate = growth / timeSpan; // bytes per ms
72
73 if (growthRate > 1000) { // > 1KB/s growth
74 console.warn('Potential memory leak detected:', {
75 growthRate: this.formatBytes(growthRate * 1000) + '/s',
76 totalGrowth: this.formatBytes(growth)
77 });
78 }
79 }
80 }
81
82 triggerMemoryCleanup() {
83 // Force garbage collection if available (dev tools)
84 if (window.gc) {
85 window.gc();
86 console.log('Forced garbage collection');
87 }
88
89 // Cleanup event
90 window.dispatchEvent(new CustomEvent('memoryCleanup', {
91 detail: { memoryUsage: this.getMemoryInfo() }
92 }));
93 }
94
95 generateMemoryReport() {
96 if (this.measurements.length === 0) {
97 return { error: 'No measurements available' };
98 }
99
100 const latest = this.measurements[this.measurements.length - 1];
101 const oldest = this.measurements[0];
102
103 return {
104 currentUsage: {
105 used: this.formatBytes(latest.usedJSHeapSize),
106 total: this.formatBytes(latest.totalJSHeapSize),
107 limit: this.formatBytes(latest.jsHeapSizeLimit),
108 percent: latest.usedPercent.toFixed(2) + '%'
109 },
110 trend: {
111 totalGrowth: this.formatBytes(latest.usedJSHeapSize - oldest.usedJSHeapSize),
112 timeSpan: this.formatTime(latest.timestamp - oldest.timestamp),
113 measurementCount: this.measurements.length
114 },
115 recommendations: this.generateRecommendations()
116 };
117 }
118
119 generateRecommendations() {
120 const recommendations = [];
121 const latest = this.measurements[this.measurements.length - 1];
122
123 if (latest.usedPercent > 80) {
124 recommendations.push('Memory usage is very high - consider implementing cleanup strategies');
125 }
126
127 if (this.measurements.length > 5) {
128 const recentGrowth = this.measurements.slice(-5);
129 const avgGrowth = recentGrowth.reduce((sum, m, i) =>
130 i === 0 ? 0 : sum + (m.usedJSHeapSize - recentGrowth[i-1].usedJSHeapSize), 0
131 ) / (recentGrowth.length - 1);
132
133 if (avgGrowth > 1024 * 1024) { // > 1MB average growth
134 recommendations.push('Consistent memory growth detected - check for memory leaks');
135 }
136 }
137
138 return recommendations;
139 }
140
141 formatBytes(bytes) {
142 const units = ['B', 'KB', 'MB', 'GB'];
143 let size = bytes;
144 let unitIndex = 0;
145
146 while (size >= 1024 && unitIndex < units.length - 1) {
147 size /= 1024;
148 unitIndex++;
149 }
150
151 return `${size.toFixed(2)} ${units[unitIndex]}`;
152 }
153
154 formatTime(ms) {
155 const seconds = Math.floor(ms / 1000);
156 const minutes = Math.floor(seconds / 60);
157 const hours = Math.floor(minutes / 60);
158
159 if (hours > 0) return `${hours}h ${minutes % 60}m`;
160 if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
161 return `${seconds}s`;
162 }
163
164 trimMeasurements() {
165 // Keep only last 100 measurements
166 if (this.measurements.length > 100) {
167 this.measurements = this.measurements.slice(-100);
168 }
169 }
170}
171
172// Memory leak detector
173class MemoryLeakDetector {
174 constructor() {
175 this.snapshots = [];
176 this.leakPatterns = new Map();
177 }
178
179 takeSnapshot(label = 'snapshot') {
180 const snapshot = {
181 label: label,
182 timestamp: Date.now(),
183 memory: performance.memory ? {
184 used: performance.memory.usedJSHeapSize,
185 total: performance.memory.totalJSHeapSize
186 } : null,
187 objectCounts: this.countObjects()
188 };
189
190 this.snapshots.push(snapshot);
191 console.log(`Memory snapshot taken: ${label}`);
192
193 return snapshot;
194 }
195
196 countObjects() {
197 // Comptage approximatif des objets (en pratique, utilise les dev tools)
198 const counts = {
199 arrays: 0,
200 objects: 0,
201 functions: 0,
202 strings: 0
203 };
204
205 // Simulation - dans une vraie implémentation, utilise l'API heap snapshot
206 return counts;
207 }
208
209 compareSnapshots(snapshot1Label, snapshot2Label) {
210 const snap1 = this.snapshots.find(s => s.label === snapshot1Label);
211 const snap2 = this.snapshots.find(s => s.label === snapshot2Label);
212
213 if (!snap1 || !snap2) {
214 console.error('Snapshots not found');
215 return null;
216 }
217
218 const comparison = {
219 memoryGrowth: snap2.memory ? snap2.memory.used - snap1.memory.used : 0,
220 timeSpan: snap2.timestamp - snap1.timestamp,
221 objectGrowth: {},
222 suspiciousGrowth: []
223 };
224
225 // Analyse de la croissance des objets
226 Object.keys(snap1.objectCounts).forEach(type => {
227 const growth = snap2.objectCounts[type] - snap1.objectCounts[type];
228 comparison.objectGrowth[type] = growth;
229
230 if (growth > 100) { // Suspicious growth threshold
231 comparison.suspiciousGrowth.push({
232 type: type,
233 growth: growth,
234 growthRate: growth / (comparison.timeSpan / 1000) // per second
235 });
236 }
237 });
238
239 return comparison;
240 }
241
242 detectLeakPatterns() {
243 if (this.snapshots.length < 3) return [];
244
245 const patterns = [];
246 const recent = this.snapshots.slice(-3);
247
248 // Check for consistent memory growth
249 const memoryGrowth = recent.map((snap, i) =>
250 i === 0 ? 0 : snap.memory.used - recent[i-1].memory.used
251 ).slice(1);
252
253 const consistentGrowth = memoryGrowth.every(growth => growth > 0);
254
255 if (consistentGrowth) {
256 const avgGrowth = memoryGrowth.reduce((a, b) => a + b) / memoryGrowth.length;
257
258 patterns.push({
259 type: 'consistent_memory_growth',
260 severity: avgGrowth > 1024 * 1024 ? 'high' : 'medium',
261 description: `Memory consistently growing by ${(avgGrowth / 1024 / 1024).toFixed(2)}MB per measurement`,
262 avgGrowth: avgGrowth
263 });
264 }
265
266 return patterns;
267 }
268}
269
270// Global memory monitor instance
271const memoryMonitor = new MemoryMonitor();
272const leakDetector = new MemoryLeakDetector();
273
274// Usage example
275function startMemoryAnalysis() {
276 memoryMonitor.startMonitoring(2000); // Every 2 seconds
277
278 // Take initial snapshot
279 leakDetector.takeSnapshot('initial');
280
281 // Simulate some operations and take snapshots
282 setTimeout(() => {
283 // Simulate memory allocation
284 const data = new Array(100000).fill('test data');
285 leakDetector.takeSnapshot('after_allocation');
286 }, 5000);
287
288 setTimeout(() => {
289 // Generate memory report
290 const report = memoryMonitor.generateMemoryReport();
291 console.log('Memory Report:', report);
292
293 const patterns = leakDetector.detectLeakPatterns();
294 console.log('Leak Patterns:', patterns);
295 }, 10000);
296}performance.memory ne fonctionne que dans Chromium et donne des valeurs approximatives, et window.gc n'existe que si le navigateur est lancé avec le flag --js-flags=--expose-gc. Les DevTools offrent un diagnostic plus sûr : ouvre le panneau Memory, prends un heap snapshot, répète l'action suspecte, prends un deuxième snapshot, compare-les et repère les objets retenus en mémoire (retained objects).
Best Practices pour le Memory Management
Proactive Memory Management
Mieux vaut prévenir que guérir. Un composant doté d'un budget mémoire nettoie les ressources inutilisées, un pool d'objets (object pooling) les réutilise, et WeakRef permet de garder un cache sans bloquer le GC :
1// Memory-aware component design
2class MemoryAwareComponent {
3 constructor(config) {
4 this.config = config;
5 this.resources = new Map();
6 this.cleanup = new Set();
7 this.memoryBudget = config.memoryBudget || 10 * 1024 * 1024; // 10MB default
8 }
9
10 // Resource tracking
11 allocateResource(key, factory, size = 0) {
12 if (this.getCurrentMemoryUsage() + size > this.memoryBudget) {
13 this.performCleanup();
14 }
15
16 const resource = factory();
17 this.resources.set(key, {
18 data: resource,
19 size: size,
20 allocatedAt: Date.now(),
21 lastAccessed: Date.now()
22 });
23
24 return resource;
25 }
26
27 getResource(key) {
28 const resource = this.resources.get(key);
29 if (resource) {
30 resource.lastAccessed = Date.now();
31 return resource.data;
32 }
33 return null;
34 }
35
36 getCurrentMemoryUsage() {
37 return Array.from(this.resources.values())
38 .reduce((total, resource) => total + resource.size, 0);
39 }
40
41 performCleanup() {
42 console.log('Performing memory cleanup...');
43
44 const now = Date.now();
45 const staleThreshold = 5 * 60 * 1000; // 5 minutes
46
47 // Remove stale resources
48 for (const [key, resource] of this.resources) {
49 if (now - resource.lastAccessed > staleThreshold) {
50 this.deallocateResource(key);
51 }
52 }
53
54 // Run custom cleanup functions
55 this.cleanup.forEach(cleanupFn => {
56 try {
57 cleanupFn();
58 } catch (error) {
59 console.error('Cleanup function failed:', error);
60 }
61 });
62 }
63
64 deallocateResource(key) {
65 const resource = this.resources.get(key);
66 if (resource) {
67 // Custom cleanup if resource has cleanup method
68 if (resource.data && typeof resource.data.cleanup === 'function') {
69 resource.data.cleanup();
70 }
71
72 this.resources.delete(key);
73 console.log(`Deallocated resource: ${key}`);
74 }
75 }
76
77 addCleanupFunction(fn) {
78 this.cleanup.add(fn);
79 }
80
81 removeCleanupFunction(fn) {
82 this.cleanup.delete(fn);
83 }
84
85 destroy() {
86 // Cleanup all resources
87 this.resources.forEach((_, key) => {
88 this.deallocateResource(key);
89 });
90
91 // Run all cleanup functions
92 this.cleanup.forEach(cleanupFn => {
93 try {
94 cleanupFn();
95 } catch (error) {
96 console.error('Cleanup function failed during destroy:', error);
97 }
98 });
99
100 this.cleanup.clear();
101 console.log('Component destroyed and cleaned up');
102 }
103}
104
105// Object pooling pour les objets utilisés fréquemment
106class ObjectPool {
107 constructor(factory, resetFn, initialSize = 10) {
108 this.factory = factory;
109 this.resetFn = resetFn;
110 this.pool = [];
111 this.activeObjects = new Set();
112
113 // Pre-allocate objects
114 for (let i = 0; i < initialSize; i++) {
115 this.pool.push(this.factory());
116 }
117 }
118
119 acquire() {
120 let obj;
121
122 if (this.pool.length > 0) {
123 obj = this.pool.pop();
124 } else {
125 obj = this.factory();
126 console.log('Created new object (pool exhausted)');
127 }
128
129 this.activeObjects.add(obj);
130 return obj;
131 }
132
133 release(obj) {
134 if (this.activeObjects.has(obj)) {
135 this.activeObjects.delete(obj);
136
137 // Reset object to initial state
138 if (this.resetFn) {
139 this.resetFn(obj);
140 }
141
142 this.pool.push(obj);
143 }
144 }
145
146 getStats() {
147 return {
148 poolSize: this.pool.length,
149 activeObjects: this.activeObjects.size,
150 totalObjects: this.pool.length + this.activeObjects.size
151 };
152 }
153}
154
155// Exemple d'utilisation de l'object pooling
156const vectorPool = new ObjectPool(
157 () => ({ x: 0, y: 0, z: 0 }), // factory
158 (vector) => { vector.x = 0; vector.y = 0; vector.z = 0; }, // reset
159 50 // initial size
160);
161
162function performVectorCalculations() {
163 const vectors = [];
164
165 // Acquire vectors from pool instead of creating new ones
166 for (let i = 0; i < 100; i++) {
167 const vector = vectorPool.acquire();
168 vector.x = Math.random();
169 vector.y = Math.random();
170 vector.z = Math.random();
171 vectors.push(vector);
172 }
173
174 // Do calculations...
175
176 // Release vectors back to pool
177 vectors.forEach(vector => vectorPool.release(vector));
178
179 console.log('Vector pool stats:', vectorPool.getStats());
180}
181
182// WeakRef pour un caching optionnel
183class WeakRefCache {
184 constructor() {
185 this.cache = new Map();
186 this.finalizationRegistry = new FinalizationRegistry((key) => {
187 console.log(`Object with key '${key}' was garbage collected`);
188 this.cache.delete(key);
189 });
190 }
191
192 set(key, value) {
193 const weakRef = new WeakRef(value);
194 this.cache.set(key, weakRef);
195 this.finalizationRegistry.register(value, key);
196 }
197
198 get(key) {
199 const weakRef = this.cache.get(key);
200 if (!weakRef) return null;
201
202 const value = weakRef.deref();
203 if (value === undefined) {
204 // Object was garbage collected
205 this.cache.delete(key);
206 return null;
207 }
208
209 return value;
210 }
211
212 has(key) {
213 return this.get(key) !== null;
214 }
215
216 delete(key) {
217 this.cache.delete(key);
218 }
219
220 size() {
221 // Clean up dead references
222 for (const [key, weakRef] of this.cache) {
223 if (weakRef.deref() === undefined) {
224 this.cache.delete(key);
225 }
226 }
227
228 return this.cache.size;
229 }
230}WeakRef crée une référence faible, qui ne bloque pas le garbage collection : deref() renvoie l'objet, ou undefined si le GC l'a déjà emporté. Le callback de FinalizationRegistry peut ne jamais être appelé, alors considère-le comme un nettoyage d'appoint, pas comme de la logique applicative.
Memory Profiling Tools
Pour finir, un profiler qui mesure la consommation de mémoire autour d'une méthode choisie et s'y attache grâce à un décorateur :
1// Development memory profiling utilities
2class MemoryProfiler {
3 constructor() {
4 this.profiles = new Map();
5 this.isProfilingEnabled = process.env.NODE_ENV === 'development';
6 }
7
8 startProfile(name) {
9 if (!this.isProfilingEnabled) return;
10
11 const profile = {
12 name: name,
13 startTime: performance.now(),
14 startMemory: performance.memory ? {
15 used: performance.memory.usedJSHeapSize,
16 total: performance.memory.totalJSHeapSize
17 } : null,
18 snapshots: []
19 };
20
21 this.profiles.set(name, profile);
22 console.log(`Started memory profile: ${name}`);
23 }
24
25 snapshot(profileName, label) {
26 if (!this.isProfilingEnabled) return;
27
28 const profile = this.profiles.get(profileName);
29 if (!profile) return;
30
31 const snapshot = {
32 label: label,
33 timestamp: performance.now(),
34 memory: performance.memory ? {
35 used: performance.memory.usedJSHeapSize,
36 total: performance.memory.totalJSHeapSize
37 } : null
38 };
39
40 profile.snapshots.push(snapshot);
41 console.log(`Memory snapshot: ${profileName}.${label}`);
42 }
43
44 endProfile(name) {
45 if (!this.isProfilingEnabled) return;
46
47 const profile = this.profiles.get(name);
48 if (!profile) return;
49
50 profile.endTime = performance.now();
51 profile.endMemory = performance.memory ? {
52 used: performance.memory.usedJSHeapSize,
53 total: performance.memory.totalJSHeapSize
54 } : null;
55
56 const report = this.generateProfileReport(profile);
57 console.log(`Memory profile completed: ${name}`, report);
58
59 return report;
60 }
61
62 generateProfileReport(profile) {
63 const duration = profile.endTime - profile.startTime;
64
65 let memoryGrowth = 0;
66 if (profile.startMemory && profile.endMemory) {
67 memoryGrowth = profile.endMemory.used - profile.startMemory.used;
68 }
69
70 const snapshots = profile.snapshots.map((snapshot, index) => {
71 const prevSnapshot = index > 0 ? profile.snapshots[index - 1] :
72 { memory: profile.startMemory, timestamp: profile.startTime };
73
74 return {
75 label: snapshot.label,
76 timeDelta: snapshot.timestamp - prevSnapshot.timestamp,
77 memoryDelta: snapshot.memory && prevSnapshot.memory ?
78 snapshot.memory.used - prevSnapshot.memory.used : 0
79 };
80 });
81
82 return {
83 duration: duration.toFixed(2) + 'ms',
84 memoryGrowth: memoryGrowth,
85 memoryGrowthFormatted: this.formatBytes(memoryGrowth),
86 snapshots: snapshots,
87 recommendations: this.generateProfileRecommendations(profile, memoryGrowth)
88 };
89 }
90
91 generateProfileRecommendations(profile, memoryGrowth) {
92 const recommendations = [];
93
94 if (memoryGrowth > 1024 * 1024) { // > 1MB growth
95 recommendations.push('Significant memory growth detected - review object allocations');
96 }
97
98 if (profile.snapshots.length > 0) {
99 const biggestGrowth = Math.max(...profile.snapshots.map((s, i) => {
100 const prev = i > 0 ? profile.snapshots[i-1] : { memory: profile.startMemory };
101 return s.memory && prev.memory ? s.memory.used - prev.memory.used : 0;
102 }));
103
104 if (biggestGrowth > 512 * 1024) { // > 512KB single growth
105 recommendations.push('Large single allocation detected - consider chunking or streaming');
106 }
107 }
108
109 return recommendations;
110 }
111
112 formatBytes(bytes) {
113 if (bytes === 0) return '0 B';
114 const k = 1024;
115 const sizes = ['B', 'KB', 'MB', 'GB'];
116 const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k));
117 return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
118 }
119}
120
121// Global profiler instance
122const memoryProfiler = new MemoryProfiler();
123
124// Décorateur pour le profilage automatique des fonctions
125function profileMemory(name) {
126 return function(target, propertyKey, descriptor) {
127 const originalMethod = descriptor.value;
128
129 descriptor.value = function(...args) {
130 const profileName = name || `${target.constructor.name}.${propertyKey}`;
131
132 memoryProfiler.startProfile(profileName);
133
134 try {
135 const result = originalMethod.apply(this, args);
136
137 if (result instanceof Promise) {
138 return result.finally(() => {
139 memoryProfiler.endProfile(profileName);
140 });
141 } else {
142 memoryProfiler.endProfile(profileName);
143 return result;
144 }
145 } catch (error) {
146 memoryProfiler.endProfile(profileName);
147 throw error;
148 }
149 };
150
151 return descriptor;
152 };
153}
154
155// Usage example
156class DataProcessor {
157 @profileMemory('processLargeDataset')
158 processLargeDataset(data) {
159 memoryProfiler.snapshot('processLargeDataset', 'start');
160
161 // Process data in chunks
162 const chunks = this.chunkArray(data, 1000);
163 const results = [];
164
165 chunks.forEach((chunk, index) => {
166 memoryProfiler.snapshot('processLargeDataset', `chunk_${index}`);
167
168 const processed = chunk.map(item => this.transformItem(item));
169 results.push(...processed);
170 });
171
172 memoryProfiler.snapshot('processLargeDataset', 'end');
173 return results;
174 }
175
176 chunkArray(array, size) {
177 const chunks = [];
178 for (let i = 0; i < array.length; i += size) {
179 chunks.push(array.slice(i, i + size));
180 }
181 return chunks;
182 }
183
184 transformItem(item) {
185 return { ...item, processed: true, timestamp: Date.now() };
186 }
187}@profileMemory est un décorateur dans le style de TypeScript, avec l'option experimentalDecorators. En JavaScript pur, les décorateurs sont encore une proposition au stade 3 (Stage 3), donc ce code nécessite un compilateur.
Récapitulatif
La gestion de la mémoire en JavaScript demande :
- De comprendre le Garbage Collection - comment il fonctionne et quand il se déclenche
- D'avoir conscience des Memory Leaks - leurs causes principales et comment les éviter
- Une approche proactive - monitoring et cleanup
- Les bons patterns - object pooling, WeakMap, AbortController
- Des outils de diagnostic - profiling et monitoring
Les règles clés : nettoie derrière toi les listeners, les timers et les observers, utilise WeakMap et WeakSet pour les références optionnelles, surveille la mémoire en production, applique l'object pooling aux objets alloués fréquemment, évite les variables globales accidentelles et teste ton application pour y détecter les fuites. Mon conseil : écris chaque addEventListener, setInterval et observe directement en binôme avec le code qui les supprime. Dans l'éditeur ci-dessous, tu testeras le garbage collection sur des exemples, et dans la leçon suivante, nous revenons à TypeScript et aux fichiers .d.ts.
Retiens : le garbage collector ne nettoie que ce vers quoi plus aucun chemin ne mène, alors les chemins vers les données inutiles, c'est à toi de les couper.
Code de cette leçon : index.js
1// Garbage Collection et Memory Leaks en JavaScript
2console.log("Laboratoire de gestion de la mémoire ADN");
3console.log("Analyse du garbage collection et des memory leaks\n");
4
5// ====================================
6// 1. COMMENT FONCTIONNE LA MÉMOIRE EN JAVASCRIPT
7// ====================================
8console.log("=== 1. Types de stockage des données ===\n");
9
10// Stack (valeurs primitives)
11let dinosaurCount = 5;
12let parkName = "Jurassic Park";
13let isActive = true;
14
15console.log("Mémoire stack (valeurs primitives) :");
16console.log("- dinosaurCount:", dinosaurCount);
17console.log("- parkName:", parkName);
18console.log("- isActive:", isActive);
19
20// Heap (objets et références)
21let dinosaur1 = { name: "T-Rex", age: 68000000 };
22let dinosaur2 = dinosaur1; // La même référence !
23
24console.log("\nMémoire heap (objets) :");
25console.log("dinosaur1:", dinosaur1);
26console.log("dinosaur2:", dinosaur2);
27console.log("dinosaur1 === dinosaur2:", dinosaur1 === dinosaur2);
28
29// ====================================
30// 2. GARBAGE COLLECTION
31// ====================================
32console.log("\n=== 2. Garbage Collection ===\n");
33
34// Algorithme Mark and Sweep
35function demonstrateGC() {
36 let tempDinosaur = { name: "Velociraptor", age: 75000000 };
37 console.log("Dinosaure temporaire créé :", tempDinosaur.name);
38
39 // En sortant de la fonction, tempDinosaur est marqué pour suppression
40 // Le Garbage Collector libérera automatiquement la mémoire
41}
42
43demonstrateGC();
44console.log("Fonction terminée - la mémoire sera libérée par le GC");
45
46// Références et GC
47let dino = { name: "Triceratops" };
48let dinoRef1 = dino;
49let dinoRef2 = dino;
50
51console.log("\nNombre de références vers l'objet : 3");
52dino = null;
53console.log("Après suppression de 'dino' : 2 références restantes");
54dinoRef1 = null;
55console.log("Après suppression de 'dinoRef1' : 1 référence restante");
56dinoRef2 = null;
57console.log("Après suppression de 'dinoRef2' : 0 référence - l'objet sera supprimé par le GC !");
58
59// ====================================
60// 3. MEMORY LEAKS - CAUSES COURANTES
61// ====================================
62console.log("\n=== 3. Memory Leaks - Erreurs fréquentes ===\n");
63
64// LEAK 1 : Variables globales
65console.log("LEAK 1 : Variables globales involontaires");
66function createAccidentalGlobal() {
67 // Absence de 'let', 'const' ou 'var' - crée une variable globale !
68 // accidentalDinosaur = { name: "Leaked Dino" }; // NE FAIS PAS ÇA !
69 console.log("Utilise toujours let/const/var !");
70}
71
72// LEAK 2 : Timers oubliés
73console.log("\nLEAK 2 : Timers/intervals oubliés");
74let leakyTimer;
75function demonstrateTimerLeak() {
76 const hugeData = new Array(1000000).fill("DNA sequence");
77
78 leakyTimer = setInterval(() => {
79 // hugeData est capturé dans la closure et ne sera jamais libéré !
80 console.log("Le timer tourne avec un accès à hugeData");
81 }, 1000);
82
83 // Solution : Nettoie toujours les timers
84 // clearInterval(leakyTimer);
85}
86
87console.log("Timer créé - la mémoire augmente !");
88console.log("Solution : clearInterval(leakyTimer);");
89
90// LEAK 3 : Références DOM
91console.log("\nLEAK 3 : Conserver des références DOM");
92const domReferences = [];
93function demonstrateDOMLeaks() {
94 // Simulation d'un élément DOM
95 const element = { id: "dino-card-1", data: "Massive data..." };
96
97 domReferences.push(element);
98
99 // Même si l'élément est retiré du DOM,
100 // la référence dans le tableau le garde encore en mémoire !
101 console.log("Élément ajouté au tableau de références");
102}
103
104// LEAK 4 : Closures
105console.log("\nLEAK 4 : Abus des closures");
106function createLeakyClosure() {
107 const hugeArray = new Array(1000000).fill({ dino: "data" });
108
109 return function() {
110 // La closure garde une référence vers tout hugeArray
111 console.log("Taille du tableau :", hugeArray.length);
112 };
113}
114
115const leakyFunction = createLeakyClosure();
116console.log("Closure créée - hugeArray reste en mémoire");
117
118// ====================================
119// 4. COMMENT ÉVITER LES MEMORY LEAKS
120// ====================================
121console.log("\n=== 4. Bonnes pratiques ===\n");
122
123// 1. Nettoie les timers
124function properTimerUsage() {
125 const timer = setInterval(() => {
126 console.log("Le timer tourne");
127 }, 1000);
128
129 // Nettoyage
130 setTimeout(() => {
131 clearInterval(timer);
132 console.log("Timer nettoyé");
133 }, 3000);
134}
135
136// 2. Utilise WeakMap/WeakSet
137console.log("Utilise WeakMap pour les métadonnées :");
138const metadataMap = new WeakMap();
139let dinosaurObject = { name: "Stegosaurus" };
140
141metadataMap.set(dinosaurObject, { discovered: 1877, location: "Colorado" });
142console.log("Métadonnées ajoutées à la WeakMap");
143
144dinosaurObject = null; // L'objet peut être supprimé par le GC !
145console.log("Objet supprimé - les métadonnées le seront aussi");
146
147// 3. Nettoyage correct des event listeners
148class DinosaurMonitor {
149 constructor() {
150 this.dinosaurs = [];
151 this.eventHandler = this.onUpdate.bind(this);
152 }
153
154 onUpdate(data) {
155 this.dinosaurs.push(data);
156 }
157
158 start() {
159 // addEventListener('update', this.eventHandler);
160 console.log("Monitor démarré avec bind()");
161 }
162
163 stop() {
164 // removeEventListener('update', this.eventHandler);
165 console.log("Monitor arrêté - event listener retiré");
166 }
167}
168
169// 4. Annule les références
170console.log("\nMets à null les gros objets quand ils sont inutiles :");
171let bigDinosaurData = new Array(100000).fill({ genome: "ATCG..." });
172console.log("Gros objet créé");
173
174// Quand tu as fini de travailler avec les données :
175bigDinosaurData = null;
176console.log("Objet mis à null - prêt pour le GC");
177
178// ====================================
179// 5. SURVEILLANCE DE LA MÉMOIRE
180// ====================================
181console.log("\n=== 5. Surveillance de la mémoire ===\n");
182
183if (performance.memory) {
184 console.log("Informations sur la mémoire :");
185 console.log("- Used JS Heap:", performance.memory.usedJSHeapSize);
186 console.log("- Total JS Heap:", performance.memory.totalJSHeapSize);
187 console.log("- Limit:", performance.memory.jsHeapSizeLimit);
188} else {
189 console.log("performance.memory indisponible (utilise les Chrome DevTools)");
190}
191
192console.log("\nOutils de débogage :");
193console.log("1. Chrome DevTools → Memory → Heap Snapshot");
194console.log("2. Performance profiler");
195console.log("3. Memory timeline");
196
197console.log("\nSouviens-toi :");
198console.log("- Nettoie toujours les timers et event listeners");
199console.log("- Utilise WeakMap/WeakSet quand c'est possible");
200console.log("- Mets à null les gros objets après usage");
201console.log("- Évite les variables globales");
202console.log("- Surveille la consommation mémoire dans les DevTools");Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quelle est la cause la plus fréquente de fuites mémoire en JavaScript ?
2. Quel mécanisme est la principale stratégie de Garbage Collection en JavaScript moderne ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Éditeur de code
Trouve et corrige la fuite mémoire dans le code fourni.
- Éditeur de code
Supprime les références inutiles (collections globales, closures) et les event listeners jamais retirés.
- Tri vertical
Ordonne dans le bon ordre les phases du processus de garbage collection :
- Tri vertical
Ordonne dans le bon ordre les étapes d'identification d'une fuite mémoire dans une application :