SavefileArchive
USD/IDR ...
|
BTC ...
|
ETH ...
|
GOLD/gram ...
Terbaru
SavefileArchive — Tutorial coding, tips programming, dan dunia musik untuk developer & pecinta musik Indonesia
Memory Leak di Node.js: Cara Deteksi, Debug, dan Fix

Memory Leak di Node.js: Cara Deteksi, Debug, dan Fix

Memory Leak di Node.js: Cara Deteksi, Debug, dan Fix

Ilustrasi Memory Leak Node.js

Server Node.js kamu berjalan normal selama beberapa jam, lalu mulai melambat, dan akhirnya crash dengan error JavaScript heap out of memory. Setelah restart, siklus yang sama berulang. Ini adalah tanda klasik memory leak — dan ini adalah salah satu bug yang paling sulit ditemukan karena tidak langsung terlihat.


1. Tanda-tanda Memory Leak

# Cek memory usage Node.js process
ps aux | grep node
# Lihat kolom RSS (Resident Set Size) — jika terus naik, ada leak

# Atau gunakan htop dan filter proses node
htop -p $(pgrep node)

# Di dalam aplikasi, log memory usage secara berkala
setInterval(() => {
  const used = process.memoryUsage();
  console.log({
    heapUsed: `${Math.round(used.heapUsed / 1024 / 1024)} MB`,
    heapTotal: `${Math.round(used.heapTotal / 1024 / 1024)} MB`,
    rss: `${Math.round(used.rss / 1024 / 1024)} MB`,
  });
}, 30000); // Log setiap 30 detik

2. Penyebab Memory Leak Paling Umum

Penyebab 1: Event Listener yang Tidak Di-remove

// ❌ Memory leak — listener menumpuk setiap kali fungsi dipanggil
function setupHandler(emitter) {
  emitter.on('data', (data) => {
    processData(data);
  });
  // Listener tidak pernah di-remove!
}

// Setiap kali setupHandler dipanggil, listener baru ditambahkan
// Setelah 1000 request → 1000 listener aktif

// ✅ Fix: simpan referensi dan remove saat tidak dibutuhkan
function setupHandler(emitter) {
  const handler = (data) => processData(data);
  emitter.on('data', handler);
  
  // Return cleanup function
  return () => emitter.off('data', handler);
}

// Atau gunakan once() jika hanya butuh satu kali
emitter.once('data', handler);

// Cek jumlah listener (default max 10, warning jika lebih)
console.log(emitter.listenerCount('data'));
emitter.setMaxListeners(20); // Naikkan limit jika memang butuh banyak listener

Penyebab 2: Cache Tanpa Batas

// ❌ Cache yang terus tumbuh tanpa batas
const cache = new Map();

app.get('/user/:id', async (req, res) => {
  const { id } = req.params;
  
  if (!cache.has(id)) {
    const user = await db.users.findById(id);
    cache.set(id, user);  // Cache terus bertambah, tidak pernah dihapus!
  }
  
  res.json(cache.get(id));
});

// ✅ Fix: gunakan LRU cache dengan batas ukuran
import LRU from 'lru-cache';

const cache = new LRU({
  max: 500,           // Maksimal 500 item
  ttl: 1000 * 60 * 5, // TTL 5 menit
});

app.get('/user/:id', async (req, res) => {
  const { id } = req.params;
  
  const cached = cache.get(id);
  if (cached) return res.json(cached);
  
  const user = await db.users.findById(id);
  cache.set(id, user);
  res.json(user);
});

Penyebab 3: Closure yang Menahan Referensi Besar

// ❌ Closure menahan referensi ke data besar
function processLargeData() {
  const hugeArray = new Array(1000000).fill('data'); // 1 juta item
  
  // Closure ini menahan referensi ke hugeArray
  // meskipun hugeArray tidak dipakai di dalam callback!
  setTimeout(() => {
    console.log('Done');
    // hugeArray tidak bisa di-GC karena closure masih hidup
  }, 5000);
}

// ✅ Fix: null-kan referensi yang tidak dibutuhkan
function processLargeData() {
  let hugeArray = new Array(1000000).fill('data');
  
  // Proses data
  const result = hugeArray.reduce((acc, item) => acc + item.length, 0);
  
  hugeArray = null; // Bebaskan referensi sebelum async operation
  
  setTimeout(() => {
    console.log('Done, result:', result); // Hanya simpan result, bukan array
  }, 5000);
}

Penyebab 4: Database Connection yang Tidak Ditutup

// ❌ Connection pool habis karena connection tidak di-release
app.get('/data', async (req, res) => {
  const client = await pool.connect();
  const result = await client.query('SELECT * FROM users');
  // Lupa client.release()! Connection tidak kembali ke pool
  res.json(result.rows);
});

// ✅ Fix: selalu release connection, bahkan saat error
app.get('/data', async (req, res) => {
  const client = await pool.connect();
  try {
    const result = await client.query('SELECT * FROM users');
    res.json(result.rows);
  } finally {
    client.release(); // Selalu dieksekusi, bahkan jika ada error
  }
});

3. Tools untuk Deteksi Memory Leak

Node.js Built-in: --inspect flag

# Jalankan Node.js dengan inspector
node --inspect server.js

# Buka Chrome → chrome://inspect
# Klik "inspect" pada proses Node.js
# Pergi ke tab "Memory" → Take Heap Snapshot
# Jalankan beberapa request → Take Heap Snapshot lagi
# Bandingkan dua snapshot untuk melihat apa yang bertambah

clinic.js — Profiling Mudah

# Install
npm install -g clinic

# Jalankan dengan doctor (deteksi masalah umum)
clinic doctor -- node server.js

# Jalankan dengan heapprofiler (analisis memory)
clinic heapprofiler -- node server.js

# Setelah selesai, buka laporan HTML yang dihasilkan
# clinic akan menunjukkan grafik memory usage dan hotspot

memwatch-next — Alert Otomatis

// npm install @airbnb/node-memwatch
import memwatch from '@airbnb/node-memwatch';

// Alert saat heap terus naik selama 5 GC berturut-turut
memwatch.on('leak', (info) => {
  console.error('Memory leak terdeteksi!', info);
  // info: { start, end, growth, reason }
  // Kirim alert ke Slack/PagerDuty di sini
});

// Bandingkan heap snapshot
const hd = new memwatch.HeapDiff();

// ... jalankan operasi yang dicurigai ...

const diff = hd.end();
console.log(JSON.stringify(diff, null, 2));
// Menampilkan objek apa yang bertambah dan berapa banyak

4. Pencegahan Memory Leak

// 1. Selalu gunakan WeakMap/WeakSet untuk cache yang terkait dengan objek
//    WeakMap tidak mencegah GC pada key-nya
const cache = new WeakMap();

// 2. Gunakan AbortController untuk cancel async operation
const controller = new AbortController();
const { signal } = controller;

fetch('/api/data', { signal })
  .then(res => res.json())
  .catch(err => {
    if (err.name === 'AbortError') return; // Request di-cancel, bukan error
    throw err;
  });

// Cancel saat tidak dibutuhkan (misal: component unmount di React)
controller.abort();

// 3. Monitor memory di production
// Tambahkan metric ke Prometheus/Datadog
setInterval(() => {
  const mem = process.memoryUsage();
  metrics.gauge('nodejs.heap.used', mem.heapUsed);
  metrics.gauge('nodejs.heap.total', mem.heapTotal);
}, 10000);

Kesimpulan

Memory leak di Node.js hampir selalu disebabkan oleh salah satu dari empat pola: event listener yang tidak di-remove, cache tanpa batas, closure yang menahan referensi besar, atau resource yang tidak di-release. Gunakan --inspect dan Chrome DevTools untuk analisis heap snapshot, atau clinic.js untuk profiling yang lebih mudah. Yang terpenting: monitor memory usage di production secara aktif — jangan tunggu server crash untuk tahu ada masalah.