Zarządzanie pamięcią w JavaScript jest jednym z najważniejszych aspektów wydajności aplikacji. Mimo że JavaScript posiada automatyczny garbage collector, deweloperzy muszą zrozumieć, jak działa pamięć, aby unikać memory leaks i optymalizować wydajność swoich aplikacji.
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();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}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}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 sekundę
170 setInterval(() => {
171 genGC.autoGC();
172 console.log('Generation stats:', genGC.getGenerationStats());
173 }, 2000);
174}1// Problem: Niekontrolowane globalne zmienne
2class GlobalVariableLeaks {
3 static demonstrateLeaks() {
4 // BAD: Przypadkowe globalne zmienne
5 function accidentalGlobal() {
6 // Brak 'var', 'let', 'const' - tworzy globalną zmienną!
7 leakedVariable = 'This will leak!';
8
9 // Też globalnie przez 'this' w non-strict mode
10 this.anotherLeak = { data: new Array(1000).fill('leak') };
11 }
12
13 accidentalGlobal();
14
15 // Te zmienne pozostaną w pamięci do końca działania aplikacji
16 console.log('Leaked variables:', window.leakedVariable, window.anotherLeak);
17 }
18
19 static demonstrateSolutions() {
20 // GOOD: Używaj strict mode
21 'use strict';
22
23 function properFunction() {
24 // Błąd kompilacji zamiast globalnej zmiennej
25 // notDeclared = 'This will throw error in strict mode';
26
27 let properVariable = 'This is properly scoped';
28 const anotherProper = { data: 'safe' };
29
30 return { properVariable, anotherProper };
31 }
32
33 // GOOD: Moduł pattern
34 const safeModule = (function() {
35 let privateData = [];
36
37 return {
38 addData: function(item) {
39 privateData.push(item);
40 },
41
42 getData: function() {
43 return privateData.slice(); // Return copy
44 },
45
46 clear: function() {
47 privateData = [];
48 }
49 };
50 })();
51
52 return { safeModule };
53 }
54}
55
56// 2. Event Listeners
57class EventListenerLeaks {
58 constructor() {
59 this.data = new Array(10000).fill('memory consuming data');
60 this.handlers = new Map();
61 }
62
63 // BAD: Event listeners bez cleanup
64 addLeakyListeners() {
65 const button = document.getElementById('leaky-button');
66
67 // Problem: listener referencje obiekt, który może mieć dużo danych
68 const handler = () => {
69 console.log('Clicked!', this.data.length);
70 };
71
72 button?.addEventListener('click', handler);
73
74 // Kiedy komponent jest usuwany, listener pozostaje!
75 // Cały obiekt nie może być zebrany przez GC
76 }
77
78 // GOOD: Proper cleanup
79 addProperListeners() {
80 const button = document.getElementById('proper-button');
81
82 const handler = (event) => {
83 console.log('Proper click!', event.target.id);
84 };
85
86 this.handlers.set('button-click', handler);
87 button?.addEventListener('click', handler);
88 }
89
90 cleanup() {
91 // Zawsze usuń event listeners podczas cleanup
92 this.handlers.forEach((handler, key) => {
93 if (key === 'button-click') {
94 const button = document.getElementById('proper-button');
95 button?.removeEventListener('click', handler);
96 }
97 });
98
99 this.handlers.clear();
100 console.log('Event listeners cleaned up');
101 }
102
103 // Modern approach: AbortController
104 addModernListeners() {
105 const controller = new AbortController();
106 const signal = controller.signal;
107
108 document.addEventListener('click', (event) => {
109 console.log('Modern click!', event.target);
110 }, { signal });
111
112 // Cleanup wszystkich listeners na raz
113 setTimeout(() => {
114 controller.abort();
115 console.log('All listeners aborted');
116 }, 10000);
117 }
118}
119
120// 3. Timers i Intervals
121class TimerLeaks {
122 constructor() {
123 this.data = new Array(100000).fill('timer data');
124 this.timers = new Set();
125 }
126
127 // BAD: Timers bez cleanup
128 createLeakyTimers() {
129 // Problem: setInterval referencje całą klasę
130 const intervalId = setInterval(() => {
131 console.log('Leaky timer tick', this.data.length);
132 }, 1000);
133
134 // Timer nigdy nie zostaje wyczyszczony!
135 // Obiekt nie może być zebrany przez GC
136
137 return intervalId;
138 }
139
140 // GOOD: Tracked timers
141 createTrackedTimers() {
142 const intervalId = setInterval(() => {
143 console.log('Tracked timer tick');
144 }, 1000);
145
146 this.timers.add(intervalId);
147
148 // Auto cleanup po czasie
149 const timeoutId = setTimeout(() => {
150 this.clearTimer(intervalId);
151 }, 10000);
152
153 this.timers.add(timeoutId);
154
155 return intervalId;
156 }
157
158 clearTimer(timerId) {
159 clearInterval(timerId);
160 clearTimeout(timerId);
161 this.timers.delete(timerId);
162 console.log(`Timer ${timerId} cleared`);
163 }
164
165 cleanup() {
166 this.timers.forEach(timerId => {
167 clearInterval(timerId);
168 clearTimeout(timerId);
169 });
170
171 this.timers.clear();
172 console.log('All timers cleaned up');
173 }
174
175 // Modern approach: AbortController dla async operations
176 createAbortableTimer() {
177 const controller = new AbortController();
178
179 const timer = {
180 start: () => {
181 const interval = setInterval(() => {
182 if (controller.signal.aborted) {
183 clearInterval(interval);
184 return;
185 }
186
187 console.log('Abortable timer tick');
188 }, 1000);
189
190 return interval;
191 },
192
193 stop: () => {
194 controller.abort();
195 }
196 };
197
198 return timer;
199 }
200}
201
202// 4. Closures
203class ClosureLeaks {
204 static demonstrateLeaks() {
205 function createLeakyClosures() {
206 const largeData = new Array(1000000).fill('leak data');
207
208 // Problem: closure trzyma referencję do całego scope
209 return {
210 // Nawet jeśli używamy tylko małej części danych
211 getSmallPart: function() {
212 return largeData.slice(0, 10);
213 },
214
215 // Ta funkcja też trzyma całe largeData w pamięci
216 getLength: function() {
217 return largeData.length;
218 }
219 };
220 }
221
222 const leaky = createLeakyClosures();
223 console.log('Small part:', leaky.getSmallPart());
224
225 // largeData pozostaje w pamięci mimo że potrzebujemy tylko length!
226 return leaky;
227 }
228
229 static demonstrateSolutions() {
230 function createOptimizedClosures() {
231 const largeData = new Array(1000000).fill('optimized data');
232
233 // GOOD: Wyciągnij tylko potrzebne dane
234 const smallPart = largeData.slice(0, 10);
235 const dataLength = largeData.length;
236
237 // largeData może być zebrany przez GC
238
239 return {
240 getSmallPart: function() {
241 return smallPart;
242 },
243
244 getLength: function() {
245 return dataLength;
246 }
247 };
248 }
249
250 // GOOD: Null references when done
251 function createCleanClosures() {
252 let data = new Array(100000).fill('clean data');
253
254 const api = {
255 process: function() {
256 const result = data.map(item => item.toUpperCase());
257 data = null; // Explicit cleanup!
258 return result;
259 }
260 };
261
262 return api;
263 }
264
265 return { createOptimizedClosures, createCleanClosures };
266 }
267}
268
269// 5. DOM References
270class DOMReferenceLeaks {
271 constructor() {
272 this.domReferences = new Map();
273 this.observers = [];
274 }
275
276 // BAD: Holding DOM references
277 createDOMLeaks() {
278 // Problem: referencje do usuniętych elementów DOM
279 const elements = document.querySelectorAll('.dynamic-content');
280
281 elements.forEach((element, index) => {
282 this.domReferences.set(`element_${index}`, {
283 node: element, // Direct reference!
284 data: new Array(1000).fill(`data_${index}`)
285 });
286 });
287
288 // Nawet jeśli elementy zostaną usunięte z DOM,
289 // pozostaną w pamięci przez te referencje!
290 }
291
292 // GOOD: WeakMap dla DOM references
293 createSafeDOMReferences() {
294 const elementData = new WeakMap();
295
296 const elements = document.querySelectorAll('.safe-content');
297
298 elements.forEach((element, index) => {
299 // WeakMap pozwala na GC elementu gdy zostanie usunięty z DOM
300 elementData.set(element, {
301 id: index,
302 data: new Array(1000).fill(`safe_data_${index}`)
303 });
304 });
305
306 return elementData;
307 }
308
309 // Proper cleanup dla DOM observers
310 setupDOMObserver() {
311 const observer = new MutationObserver((mutations) => {
312 mutations.forEach(mutation => {
313 console.log('DOM changed:', mutation.type);
314 });
315 });
316
317 observer.observe(document.body, {
318 childList: true,
319 subtree: true
320 });
321
322 this.observers.push(observer);
323
324 // Auto cleanup
325 setTimeout(() => {
326 this.cleanupObservers();
327 }, 30000);
328 }
329
330 cleanupObservers() {
331 this.observers.forEach(observer => {
332 observer.disconnect();
333 });
334
335 this.observers = [];
336 console.log('DOM observers cleaned up');
337 }
338
339 cleanup() {
340 this.domReferences.clear();
341 this.cleanupObservers();
342 }
343}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}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}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}Zarządzanie pamięcią w JavaScript wymaga:
Właściwe zarządzanie pamięcią to klucz do wydajnych, skalowalnych aplikacji JavaScript.