Kurs JavaScript i TypeScript · Moduł 10: TypeScript w praktyce
Zarządzanie pamięcią - garbage collection, memory leaks
W tej lekcji6
Wyobraź sobie wybieg, z którego nikt nie wywozi resztek karmy. Przez kilka dni nic nie widać, a po miesiącu T-Rex nie ma gdzie stanąć. Tak działa wyciek pamięci (memory leak): aplikacja trzyma dane, których już nie potrzebuje, i z każdą godziną zwalnia. Zarządzanie pamięcią jest jednym z najważniejszych aspektów wydajności. Mimo że JavaScript ma automatyczny garbage collector, musisz rozumieć, jak działa pamięć, aby unikać wycieków.
Jak działa pamięć w JavaScript
Memory lifecycle
Każda wartość przechodzi trzy etapy: alokację, użycie i zwolnienie. Dwa pierwsze widzisz w kodzie, trzeci wykonuje za Ciebie garbage collector (GC):
1// Cykl życia pamięci w JavaScript
2const memoryLifecycle = {
3 // 1. Alokacja pamięci - automatyczna
4 allocation: () => {
5 const obj = { name: 'John', age: 30 }; // Alokacja w heap
6 const arr = [1, 2, 3, 4, 5]; // Alokacja w heap
7 const num = 42; // Alokacja w stack (primitive)
8
9 return { obj, arr, num };
10 },
11
12 // 2. Użycie pamięci - odczyt/zapis
13 usage: (data) => {
14 data.obj.name = 'Jane'; // Użycie zaalokowanej pamięci
15 data.arr.push(6);
16
17 console.log(data.obj, data.arr);
18 },
19
20 // 3. Zwalnianie pamięci - automatyczne przez GC
21 release: () => {
22 // Gdy zmienne wychodzą poza scope, stają się kandydatami do GC
23 // GC uruchomi się automatycznie w odpowiednim momencie
24 }
25};
26
27// Demonstracja cyklu
28function demonstrateMemoryLifecycle() {
29 const data = memoryLifecycle.allocation(); // Alokacja
30 memoryLifecycle.usage(data); // Użycie
31 // Po zakończeniu funkcji, 'data' staje się kandydatem do GC
32}
33
34demonstrateMemoryLifecycle();Nie wywołujesz żadnego free(). Gdy data przestaje być osiągalne po zakończeniu funkcji, obiekt staje się kandydatem do sprzątnięcia, a moment wybierze GC.
Stack vs Heap
Często mówi się, że prymitywy żyją na stosie (stack), a obiekty na stercie (heap):
1// Stack - przechowuje prymitywy i referencje
2function stackExample() {
3 const a = 5; // Stack
4 const b = 'hello'; // Stack (reference do string w heap)
5 const c = true; // Stack
6
7 console.log('Stack variables:', a, b, c);
8} // Po zakończeniu funkcji, wszystko ze stack zostaje usunięte
9
10// Heap - przechowuje obiekty i tablice
11function heapExample() {
12 const obj = { x: 1, y: 2 }; // Obiekt w heap, referencja w stack
13 const arr = [1, 2, 3]; // Tablica w heap, referencja w stack
14
15 // Modyfikacja obiektu w heap
16 obj.z = 3;
17 arr.push(4);
18
19 return { obj, arr }; // Zwracamy referencje
20} // Referencje ze stack są usuwane, ale obiekty w heap pozostają
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// Przykład użycia
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 // Symulacja zwalniania pamięci
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 nie zostaje zwolniony - symulacja memory leak
90 console.log('Final memory stats:', memoryViz.getMemoryStats());
91 }, 2000);
92}To uproszczony model. Specyfikacja ECMAScript nie określa, gdzie w pamięci leżą wartości, więc decyduje silnik: na przykład napisy i zmienne złapane przez domknięcia trafiają na stertę. MemoryVisualizer tylko symuluje alokacje, ale dobrze pokazuje, czym jest wyciek: wpisem, którego nikt nie zwolni.
Garbage Collection w JavaScript
Typy algorytmów GC
Porównaj dwie strategie. Zliczanie referencji (reference counting) usuwa obiekt, gdy nic na niego nie wskazuje. Mark-and-sweep zaczyna od korzeni (roots), takich jak zmienne globalne i stos wywołań, oznacza wszystko, co da się z nich osiągnąć, a resztę usuwa:
1// 1. Reference Counting (stary algorytm)
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 // Wzajemne referencje - memory leak w 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 (nowoczesny algorytm)
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 - oznacza wszystkie osiągnalne obiekty
115 markPhase() {
116 console.log('Starting mark phase...');
117
118 // Wyczyść poprzednie oznaczenia
119 this.objects.forEach(obj => obj.marked = false);
120
121 // Oznacz wszystkie obiekty osiągnalne z 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 // Rekurencyjnie oznacz referencje
134 obj.references.forEach(refId => markRecursive(refId));
135 }
136 };
137
138 // Rozpocznij od wszystkich roots
139 this.roots.forEach(rootId => markRecursive(rootId));
140 }
141
142 // Sweep phase - usuwa nieoznaczone obiekty
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 // Pełny cykl GC
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 // Demonstracja rozwiązywania circular references
171 demonstrateCircularReferences() {
172 // Tworzenie obiektów z 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 // Tylko obj1 jest w roots
183 this.addToRoots('mark1');
184
185 console.log('Before GC - objects with circular references');
186 this.runGarbageCollection(); // Wszystkie będą oznaczone jako osiągalne
187
188 // Usuwamy z roots
189 this.removeFromRoots('mark1');
190
191 console.log('After removing from roots');
192 this.runGarbageCollection(); // Wszystkie będą zebrane mimo circular references!
193 }
194}Dwa obiekty wskazujące na siebie nawzajem oszukają zliczanie referencji, ale nie mark-and-sweep: gdy żaden korzeń do nich nie prowadzi, znikają oba. Dlatego nowoczesne silniki używają mark-and-sweep, a sam cykl referencji nie jest już wyciekiem. Po fazach mark i sweep silnik może jeszcze zagęścić pamięć (compact), żeby usunąć dziury.
Generational Garbage Collection
Większość obiektów żyje krótko, więc silniki dzielą stertę na generacje. Młodą sprzątają często i szybko, a obiekty, które przetrwają kilka cykli, trafiają do starej:
1// Nowoczesne silniki JS używają generational GC
2class GenerationalGCDemo {
3 constructor() {
4 // Różne generacje obiektów
5 this.youngGeneration = new Map(); // Nowe obiekty
6 this.oldGeneration = new Map(); // Stare obiekty
7 this.permanentGeneration = new Map(); // Bardzo stare obiekty
8
9 this.gcCycles = 0;
10 this.promotionThreshold = 3; // Ile cykli GC przeżyć aby zostać promowanym
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 - tylko 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 // Symulacja: obiekty używane w ostatnich 5 sekundach przeżywają
52 const timeSinceLastAccess = Date.now() - obj.lastAccessed;
53
54 if (timeSinceLastAccess < 5000) {
55 obj.gcSurvived++;
56 survivors.push({ id, obj });
57
58 // Promocja do 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 // Wyczyść young generation
70 this.youngGeneration.clear();
71
72 // Przywróć survivors
73 survivors.forEach(({ id, obj }) => {
74 if (!toPromote.find(p => p.id === id)) {
75 this.youngGeneration.set(id, obj);
76 }
77 });
78
79 // Promuj do 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 - wszystkie generacje
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 // Old generation ma dłuższy czas życia
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 // Automatyczny GC na podstawie heurystyk
125 autoGC() {
126 const youngSize = this.youngGeneration.size;
127 const oldSize = this.oldGeneration.size;
128
129 // Triggers dla GC
130 if (youngSize > 100) { // Dużo młodych obiektów
131 this.runMinorGC();
132 }
133
134 if (oldSize > 50 || this.gcCycles % 10 === 0) { // Dużo starych obiektów lub co 10 cykli
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// Demonstracja generational GC
150const genGC = new GenerationalGCDemo();
151
152// Symulacja allocation patterns
153function simulateAllocationPattern() {
154 // Tworzenie wielu krótkotrwałych obiektów
155 for (let i = 0; i < 50; i++) {
156 genGC.allocateObject(`temp_${i}`, `Temporary object ${i}`);
157 }
158
159 // Tworzenie kilku długotrwałych obiektów
160 for (let i = 0; i < 5; i++) {
161 const longLived = genGC.allocateObject(`long_${i}`, `Long-lived object ${i}`);
162
163 // Symulacja regularnego dostępu
164 setInterval(() => {
165 genGC.accessObject(`long_${i}`);
166 }, 1000);
167 }
168
169 // Automatyczny GC co dwie sekundy
170 setInterval(() => {
171 genGC.autoGC();
172 console.log('Generation stats:', genGC.getGenerationStats());
173 }, 2000);
174}Symulacja upraszcza jedno: prawdziwy GC nie sprawdza, kiedy ostatnio użyłeś obiektu, tylko czy jest osiągalny. V8 ma generację młodą i starą, a generacja permanent to pojęcie z dawnej Javy.
Rodzaje Memory Leaks
Zmienne globalne, listenery, timery, domknięcia i DOM
Wyciek to obiekt wciąż osiągalny, choć już go nie potrzebujesz. Oto pięć typowych źródeł wraz z poprawnymi wersjami:
1// Problem: Niekontrolowane globalne zmienne
2class GlobalVariableLeaks {
3 static demonstrateLeaks() {
4 // BAD: Przypadkowe globalne zmienne
5 // Uwaga: kod w klasie zawsze działa w trybie strict, więc tutaj obie linie rzucą błąd.
6 // Taki wyciek powstaje w zwykłym skrypcie bez 'use strict'.
7 function accidentalGlobal() {
8 // Brak 'var', 'let', 'const' - tworzy globalną zmienną!
9 leakedVariable = 'This will leak!';
10
11 // Też globalnie przez 'this' w non-strict mode
12 this.anotherLeak = { data: new Array(1000).fill('leak') };
13 }
14
15 accidentalGlobal();
16
17 // Te zmienne pozostaną w pamięci do końca działania aplikacji
18 console.log('Leaked variables:', window.leakedVariable, window.anotherLeak);
19 }
20
21 static demonstrateSolutions() {
22 // GOOD: Używaj strict mode
23 'use strict';
24
25 function properFunction() {
26 // ReferenceError zamiast globalnej zmiennej
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: Moduł pattern
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: Event listeners bez cleanup
66 addLeakyListeners() {
67 const button = document.getElementById('leaky-button');
68
69 // Problem: listener referencje obiekt, który może mieć dużo danych
70 const handler = () => {
71 console.log('Clicked!', this.data.length);
72 };
73
74 button?.addEventListener('click', handler);
75
76 // Kiedy komponent jest usuwany, listener pozostaje!
77 // Cały obiekt nie może być zebrany przez 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 // Zawsze usuń event listeners podczas 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 wszystkich listeners na raz
115 setTimeout(() => {
116 controller.abort();
117 console.log('All listeners aborted');
118 }, 10000);
119 }
120}
121
122// 3. Timers i Intervals
123class TimerLeaks {
124 constructor() {
125 this.data = new Array(100000).fill('timer data');
126 this.timers = new Set();
127 }
128
129 // BAD: Timers bez cleanup
130 createLeakyTimers() {
131 // Problem: setInterval referencje całą klasę
132 const intervalId = setInterval(() => {
133 console.log('Leaky timer tick', this.data.length);
134 }, 1000);
135
136 // Timer nigdy nie zostaje wyczyszczony!
137 // Obiekt nie może być zebrany przez 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 po czasie
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 // Modern approach: AbortController dla async operations
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 // Problem: closure trzyma referencję do całego scope
211 return {
212 // Nawet jeśli używamy tylko małej części danych
213 getSmallPart: function() {
214 return largeData.slice(0, 10);
215 },
216
217 // Ta funkcja też trzyma całe largeData w pamięci
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 pozostaje w pamięci mimo że potrzebujemy tylko length!
228 return leaky;
229 }
230
231 static demonstrateSolutions() {
232 function createOptimizedClosures() {
233 const largeData = new Array(1000000).fill('optimized data');
234
235 // GOOD: Wyciągnij tylko potrzebne dane
236 const smallPart = largeData.slice(0, 10);
237 const dataLength = largeData.length;
238
239 // largeData może być zebrany przez 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 // Problem: referencje do usuniętych elementów DOM
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 // Nawet jeśli elementy zostaną usunięte z DOM,
291 // pozostaną w pamięci przez te referencje!
292 }
293
294 // GOOD: WeakMap dla DOM references
295 createSafeDOMReferences() {
296 const elementData = new WeakMap();
297
298 const elements = document.querySelectorAll('.safe-content');
299
300 elements.forEach((element, index) => {
301 // WeakMap pozwala na GC elementu gdy zostanie usunięty z 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 // Proper cleanup dla DOM observers
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}Najczęstsza przyczyna to zapomniane event listenery i timery. Listener na obiekcie, który żyje dłużej niż komponent, na przykład na document, trzyma przy życiu wszystko, do czego sięga jego domknięcie. AbortController usuwa wiele listenerów jednym abort(), a WeakMap nie blokuje sprzątnięcia elementu DOM. W przykładzie ze zmiennymi globalnymi pamiętaj, że klasy działają w trybie strict, więc przypisanie do niezadeklarowanej zmiennej rzuci ReferenceError.
Memory Leak Detection i Monitoring
Performance API dla Memory Monitoring
Wyciek zdradza stały wzrost zużycia pamięci. Monitor co kilka sekund zapisuje rozmiar sterty i ostrzega, gdy ten rośnie bez przerwy:
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 // Przybliżone liczenie obiektów (w rzeczywistości użyj dev tools)
198 const counts = {
199 arrays: 0,
200 objects: 0,
201 functions: 0,
202 strings: 0
203 };
204
205 // Symulacja - w prawdziwej implementacji użyj heap snapshot API
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 // Analiza wzrostu obiektów
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 działa tylko w Chromium i podaje wartości przybliżone, a window.gc istnieje tylko po uruchomieniu przeglądarki z flagą --js-flags=--expose-gc. Pewniejszą diagnozę daje DevTools: otwórz panel Memory, zrób heap snapshot, powtórz podejrzaną akcję, zrób drugi snapshot, porównaj je i znajdź obiekty zatrzymane w pamięci (retained objects).
Best Practices dla Memory Management
Proactive Memory Management
Lepiej zapobiegać niż leczyć. Komponent z budżetem pamięci sprząta nieużywane zasoby, pula obiektów (object pooling) używa ich ponownie, a WeakRef pozwala trzymać cache bez blokowania 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 dla często używanych obiektów
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// Przykład użycia 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 dla optional caching
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 tworzy słabą referencję, która nie blokuje garbage collection: deref() zwraca obiekt albo undefined, gdy GC już go zabrał. Callback FinalizationRegistry może się nigdy nie wywołać, więc traktuj go jako sprzątanie pomocnicze, nie logikę aplikacji.
Memory Profiling Tools
Na koniec profiler, który mierzy zużycie pamięci wokół wybranej metody i dołącza się do niej przez dekorator:
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// Decorator dla automatycznego profilowania funkcji
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 to dekorator w stylu TypeScriptu z opcją experimentalDecorators. W czystym JavaScripcie dekoratory są wciąż propozycją na etapie Stage 3, więc ten kod wymaga kompilatora.
Podsumowanie
Zarządzanie pamięcią w JavaScript wymaga:
- Zrozumienia Garbage Collection - jak działa i kiedy się uruchamia
- Świadomości Memory Leaks - główne przyczyny i sposoby unikania
- Proaktywnego podejścia - monitoring i cleanup
- Właściwych wzorców - object pooling, WeakMap, AbortController
- Narzędzi diagnostycznych - profiling i monitoring
Kluczowe zasady: sprzątaj po sobie listenery, timery i obserwatory, używaj WeakMap i WeakSet dla opcjonalnych referencji, monitoruj pamięć na produkcji, stosuj object pooling dla często alokowanych obiektów, unikaj przypadkowych zmiennych globalnych i testuj aplikację pod kątem wycieków. Moja rada: każde addEventListener, setInterval i observe pisz od razu w parze z kodem, który je usuwa. W edytorze poniżej przetestujesz garbage collection na przykładach, a w kolejnej lekcji wracamy do TypeScriptu i plików .d.ts.
Pamiętaj: garbage collector sprząta tylko to, do czego nie prowadzi już żadna ścieżka, więc ścieżki do niepotrzebnych danych musisz przecinać sam.
Kod do tej lekcji: index.js
1// Garbage Collection i Memory Leaks w JavaScript
2console.log("Laboratorium Zarządzania Pamięcią DNA");
3console.log("Analiza garbage collection i memory leaks\n");
4
5// ====================================
6// 1. JAK DZIAŁA PAMIĘĆ W JAVASCRIPT
7// ====================================
8console.log("=== 1. Typy przechowywania danych ===\n");
9
10// Stack (wartości prymitywne)
11let dinosaurCount = 5;
12let parkName = "Jurassic Park";
13let isActive = true;
14
15console.log("Stack memory (wartości prymitywne):");
16console.log("- dinosaurCount:", dinosaurCount);
17console.log("- parkName:", parkName);
18console.log("- isActive:", isActive);
19
20// Heap (obiekty i referencje)
21let dinosaur1 = { name: "T-Rex", age: 68000000 };
22let dinosaur2 = dinosaur1; // Ta sama referencja!
23
24console.log("\nHeap memory (obiekty):");
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// Mark and Sweep Algorithm
35function demonstrateGC() {
36 let tempDinosaur = { name: "Velociraptor", age: 75000000 };
37 console.log("Stworzono tymczasowego dinozaura:", tempDinosaur.name);
38
39 // Po wyjściu z funkcji, tempDinosaur jest oznaczony do usunięcia
40 // Garbage Collector automatycznie zwolni pamięć
41}
42
43demonstrateGC();
44console.log("Funkcja zakończona - pamięć zostanie zwolniona przez GC");
45
46// Referencje i GC
47let dino = { name: "Triceratops" };
48let dinoRef1 = dino;
49let dinoRef2 = dino;
50
51console.log("\nLiczba referencji do obiektu: 3");
52dino = null;
53console.log("Po usunięciu 'dino': 2 referencje pozostają");
54dinoRef1 = null;
55console.log("Po usunięciu 'dinoRef1': 1 referencja pozostaje");
56dinoRef2 = null;
57console.log("Po usunięciu 'dinoRef2': 0 referencji - obiekt zostanie usunięty przez GC!");
58
59// ====================================
60// 3. MEMORY LEAKS - TYPOWE PRZYCZYNY
61// ====================================
62console.log("\n=== 3. Memory Leaks - Częste błędy ===\n");
63
64// LEAK 1: Zmienne globalne
65console.log("LEAK 1: Niezamierzone zmienne globalne");
66function createAccidentalGlobal() {
67 // Brak 'let', 'const' lub 'var' - tworzy globalną zmienną!
68 // accidentalDinosaur = { name: "Leaked Dino" }; // NIE RÓB TEGO!
69 console.log("Zawsze używaj let/const/var!");
70}
71
72// LEAK 2: Zapomniane timery
73console.log("\nLEAK 2: Zapomniane timery/intervals");
74let leakyTimer;
75function demonstrateTimerLeak() {
76 const hugeData = new Array(1000000).fill("DNA sequence");
77
78 leakyTimer = setInterval(() => {
79 // hugeData jest zamknięty w closure i nigdy nie zostanie zwolniony!
80 console.log("Timer działa z dostępem do hugeData");
81 }, 1000);
82
83 // Rozwiązanie: Zawsze czyść timery
84 // clearInterval(leakyTimer);
85}
86
87console.log("Timer utworzony - pamięć rośnie!");
88console.log("Rozwiązanie: clearInterval(leakyTimer);");
89
90// LEAK 3: DOM references
91console.log("\nLEAK 3: Przechowywanie referencji DOM");
92const domReferences = [];
93function demonstrateDOMLeaks() {
94 // Symulacja DOM elementu
95 const element = { id: "dino-card-1", data: "Massive data..." };
96
97 domReferences.push(element);
98
99 // Nawet jeśli element zostanie usunięty z DOM,
100 // referencja w tablicy nadal go trzyma w pamięci!
101 console.log("Element dodany do tablicy referencji");
102}
103
104// LEAK 4: Closures
105console.log("\nLEAK 4: Nadużycie closures");
106function createLeakyClosure() {
107 const hugeArray = new Array(1000000).fill({ dino: "data" });
108
109 return function() {
110 // Closure trzyma referencję do całego hugeArray
111 console.log("Rozmiar tablicy:", hugeArray.length);
112 };
113}
114
115const leakyFunction = createLeakyClosure();
116console.log("Closure utworzony - hugeArray pozostaje w pamięci");
117
118// ====================================
119// 4. JAK UNIKAĆ MEMORY LEAKS
120// ====================================
121console.log("\n=== 4. Dobre praktyki ===\n");
122
123// 1. Czyść timery
124function properTimerUsage() {
125 const timer = setInterval(() => {
126 console.log("Timer działa");
127 }, 1000);
128
129 // Cleanup
130 setTimeout(() => {
131 clearInterval(timer);
132 console.log("Timer został wyczyszczony");
133 }, 3000);
134}
135
136// 2. Używaj WeakMap/WeakSet
137console.log("Używaj WeakMap dla metadata:");
138const metadataMap = new WeakMap();
139let dinosaurObject = { name: "Stegosaurus" };
140
141metadataMap.set(dinosaurObject, { discovered: 1877, location: "Colorado" });
142console.log("Metadata dodana do WeakMap");
143
144dinosaurObject = null; // Obiekt może być usunięty przez GC!
145console.log("Obiekt usunięty - metadata również zostanie usunięta");
146
147// 3. Proper event listeners cleanup
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 started z bind()");
161 }
162
163 stop() {
164 // removeEventListener('update', this.eventHandler);
165 console.log("Monitor stopped - event listener removed");
166 }
167}
168
169// 4. Nullify references
170console.log("\nNullifikuj duże obiekty gdy nie są potrzebne:");
171let bigDinosaurData = new Array(100000).fill({ genome: "ATCG..." });
172console.log("Duży obiekt utworzony");
173
174// Gdy skończysz pracę z danymi:
175bigDinosaurData = null;
176console.log("Obiekt nullified - gotowy do GC");
177
178// ====================================
179// 5. MONITORING PAMIĘCI
180// ====================================
181console.log("\n=== 5. Monitoring pamięci ===\n");
182
183if (performance.memory) {
184 console.log("Informacje o pamięci:");
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 niedostępne (użyj Chrome DevTools)");
190}
191
192console.log("\nNarzędzia do debugowania:");
193console.log("1. Chrome DevTools → Memory → Heap Snapshot");
194console.log("2. Performance profiler");
195console.log("3. Memory timeline");
196
197console.log("\nPamiętaj:");
198console.log("- Zawsze czyść timery i event listeners");
199console.log("- Używaj WeakMap/WeakSet gdy to możliwe");
200console.log("- Nullifikuj duże obiekty po użyciu");
201console.log("- Unikaj globalnych zmiennych");
202console.log("- Monitoruj zużycie pamięci w DevTools");Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co jest najczęstszą przyczyną memory leak w JavaScript?
2. Który mechanizm jest główną strategią Garbage Collection w nowoczesnym JavaScript?
To 2 z 3 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Edytor kodu
Znajdź i napraw memory leak w podanym kodzie.
- Edytor kodu
Usuń niepotrzebne referencje (globalne kolekcje, domknięcia) i nieusunięte event listenery.
- Układanie w pionie
Ułóż fazy procesu garbage collection w odpowiedniej kolejności:
- Układanie w pionie
Ułóż etapy identyfikacji memory leak w aplikacji w odpowiedniej kolejności: