Kurs JavaScript i TypeScript · Moduł 10: TypeScript w praktyce

Zarządzanie pamięcią - garbage collection, memory leaks

25 min czytania
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:

  1. Zrozumienia Garbage Collection - jak działa i kiedy się uruchamia
  2. Świadomości Memory Leaks - główne przyczyny i sposoby unikania
  3. Proaktywnego podejścia - monitoring i cleanup
  4. Właściwych wzorców - object pooling, WeakMap, AbortController
  5. 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. 1. Co jest najczęstszą przyczyną memory leak w JavaScript?

  2. 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:

Przydatne artykuły