# Optimasi Performa Event Loop dan Memory Leak di Node.js Backend

> Diterbitkan oleh Literally Web ID pada 21 Oct 2026
> Sumber: https://literally.web.id/articles/optimasi-performa-event-loop-dan-memory-leak-di-nodejs-backend

---

## 1. Intisari Kilat: Fondasi dalam Sekali Lirik

Ingat tidak ketika pertama kali kita mendeploy aplikasi Node.js pertama ke production dan merasa semuanya baik-baik saja, sampai suatu malam *PagerDuty* berdering kencang karena *JavaScript heap out of memory*? Pengalaman itu mengajarkan kita satu hal pahit: V8 Engine tidak akan menyelamatkan kita dari kecerobohan manajemen memori dan penyalahgunaan event loop. Di level infrastruktur skala besar, kita tidak bisa lagi mengandalkan manajemen memori default atau membiarkan operasi sinkron yang berat mencekik thread tunggal Node.js. Mari kita bedah bagaimana V8 Garbage Collection bekerja di balik layar, kapan kita harus melempar tugas berat ke Worker Threads, dan bagaimana Clinic.js menyelamatkan sanity kita saat melakukan profiling di production.

> [!IMPORTANT]
> Event loop Node.js dirancang untuk I/O non-blocking, bukan untuk melakukan komputasi kriptografi intensif atau pemrosesan JSON raksasa secara sinkron. Satu saja operasi CPU-bound yang memblokir *Main Thread*, maka seluruh antrean *libuv* akan mengalami kemacetan total.

Siklus hidup memori V8 dibagi menjadi *New Space* dan *Old Space*. *New Space* berukuran kecil dan menggunakan algoritma *Scavenge* yang cepat untuk objek-objek berumur pendek. Ketika objek bertahan melintasi beberapa siklus *Scavenge*, objek tersebut dipromosikan ke *Old Space* yang dikelola oleh *Mark-Sweep-Compact*. Masalah besar muncul ketika kita membuat referensi global yang tidak disengaja—seperti *global caches*, penutupan (*closures*) yang salah, atau *event listeners* yang tidak pernah dihapus. Objek-objek ini menumpuk di *Old Space*, memicu *Garbage Collector (GC)* berjalan terus-menerus (*stop-the-world pauses*), dan akhirnya meroketkan latensi aplikasi kita. Transisi dari pemahaman teoretis ini ke strategi mitigasi di lapangan menuntut kita untuk memiliki kompas keputusan yang jelas dalam memilih tools yang tepat.

## 2. Kompas Keputusan: Kapan Memilih Solusi Ini vs Alternatif Lain

Saat menghadapi lonjakan latensi atau penggunaan RAM yang merangkak naik secara misterius, kita sering dihadapkan pada persimpangan arsitektur yang krusial. Apakah kita harus menaikkan batas limit memori V8, memecah proses dengan *Worker Threads*, atau memindahkan beban ke layanan eksternal?

- **Jika aplikasi mengalami lonjakan latensi mendadak dengan CPU utilization menyentuh 100% pada satu core**: Jangan pernah menaikkan `max-old-space-size`. Itu hanya menunda bom waktu. Segera identifikasi fungsi sinkron atau proses parsing data besar, lalu pindahkan logika tersebut ke dalam `worker_threads` atau pisahkan menjadi mikroservis berbasis Go atau Python.
- **Jika konsumsi RAM naik perlahan namun pasti (*memory leak*) tanpa lonjakan trafik yang signifikan**: Hindari langsung melakukan *restart* PM2 secara membabi buta. Gunakan Clinic.js Heap Profiler atau ambil heap snapshot menggunakan `v8.writeHeapSnapshot()` untuk melacak objek apa yang terus bertambah di *Old Space*.
- **Jika Anda mengelola sistem real-time berbasis WebSocket**: Pastikan manajemen *connection state* tidak disimpan di dalam memori heap lokal instance Node.js tunggal. Jika Anda melakukan *horizontal scaling*, gunakan Redis Pub/Sub sebagai *adapter* perantara agar event loop tidak terbebani oleh sinkronisasi state antar instance.

Ketika instrumen diagnostik sudah dipilih, langkah selanjutnya adalah memastikan parameter runtime kita disetel dengan presisi bedah agar performa mesin virtual mencapai titik optimalnya.

## 3. Contekan Konfigurasi & Parameter Paling Sering Dicari

Di dunia nyata, konfigurasi environment dan flags V8 menentukan apakah aplikasi kita akan bertahan di bawah hantaman beban *flash sale* atau justru tumbang dalam hitungan detik. Berikut adalah contekan parameter runtime Node.js dan konfigurasi PM2 yang wajib Anda terapkan di lingkungan production.

| Parameter / Flag V8 | Konfigurasi Optimal Production | Kapan Harus Dinyalakan | Kapan Harus Dihatikan / Dihindari | Konteks Infrastruktur |
| :--- | :--- | :--- | :--- | :--- |
| `--max-old-space-size` | `4096` (4GB) atau 75% dari RAM kontainer | Ketika kontainer Docker memiliki batas RAM yang ketat dan Anda ingin menghindari OOM Killer kernel Linux. | Jangan diset tanpa batas (`--max-old-space-size=0`) karena V8 akan memakan seluruh RAM sistem hingga host crash. | Kubernetes Pod / Docker Container limit. |
| `--trace-gc` | Diaktifkan saat investigasi *leak* | Saat proses debugging *memory leak* di staging atau environment pre-production. | Jangan pernah dinyalakan secara permanen di production karena *log output* akan membanjiri disk I/O. | Debugging mendalam & analisis GC pause. |
| `PM2 cluster mode` | `instances: 'max'` | Ketika aplikasi bersifat stateless dan ingin memanfaatkan seluruh core CPU fisik server. | Jangan digunakan jika aplikasi mengandalkan penyimpanan state di dalam memori lokal instance. | Nginx Load Balancing & PM2 Process Manager. |
| `UV_THREADPOOL_SIZE` | `16` atau `32` (default 4) | Ketika aplikasi sangat bergantung pada operasi filesystem (`fs`) atau enkripsi crypto yang berat. | Jangan dinaikkan secara berlebihan jika aplikasi murni I/O network-bound karena akan membebani context switching CPU. | Lingkungan I/O intensif & operasi kriptografi. |

Berikut adalah contoh file konfigurasi `ecosystem.config.js` untuk PM2 yang telah disetel untuk performa tinggi dan ketahanan memori di production:

```javascript
module.exports = {
  apps: [
    {
      name: 'node-backend-prod',
      script: './dist/server.js',
      instances: 'max',
      exec_mode: 'cluster',
      // Batasi memori per instance untuk mencegah OOM sistem secara keseluruhan
      max_memory_restart: '1G',
      env: {
        NODE_ENV: 'production',
        UV_THREADPOOL_SIZE: 16,
      },
      node_args: [
        '--max-old-space-size=1536',
        // Mengaktifkan laporan diagnostik otomatis saat terjadi kegagalan fatal
        '--diagnostic-report-dir=./reports',
        '--diagnostic-report-flag=fatal',
      ],
      // Pengaturan log dan restart policy
      merge_logs: true,
      log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
      autorestart: true,
      exp_backoff_restart_delay: 100,
    },
  ],
};
```

Menyetel konfigurasi di atas adalah separuh dari kemenangan; separuhnya lagi adalah seberapa cepat kita bisa mengeksekusi perintah diagnostik langsung di terminal saat situasi darurat melanda.

## 4. Koleksi Mantra One-Liner Siap Salin untuk Keseharian

Sebagai *Systems Architect*, saya sering mendapati junior saya kebingungan mengetik perintah diagnostik saat server sedang mengalami lonjakan beban. Simpan dan kuasai mantra *one-liner* terminal berikut di dalam kepala Anda untuk situasi darurat:

- **Menganalisis latensi event loop secara real-time menggunakan Clinic.js:**
  ```bash
  npx clinic doctor -- node dist/server.js
  ```
- **Mengambil Heap Snapshot secara manual dari proses Node.js yang sedang berjalan tanpa mematikan aplikasi:**
  ```bash
  node -e 'const v8 = require("v8"); v8.writeHeapSnapshot(); console.log("Snapshot written successfully.");'
  ```
- **Memantau penggunaan memori dan CPU proses Node.js berdasarkan PID secara instan:**
  ```bash
  ps -eo pid,ppid,%mem,%cpu,cmd --sort=-%mem | grep node
  ```
- **Menjalankan load testing cepat untuk menguji respons event loop menggunakan Autocannon:**
  ```bash
  npx autocannon -c 100 -d 30 -m GET http://localhost:3000/api/health
  ```
- **Menganalisis port yang terbuka dan koneksi TIME_WAIT yang menumpuk pada backend Node.js:**
  ```bash
  ss -s && netstat -an | grep ESTABLISHED | grep :3000 | wc -l
  ```

Namun, betapapun canggihnya mantra terminal yang Anda miliki, satu kesalahan arsitektur mendasar di level kode dapat meruntuhkan seluruh jerih payah infrastruktur dalam sekejap.

## 5. Jalur Bahaya: Dosa Besar yang Wajib Dihindari di Produksi

Pengalaman mengajarkan bahwa teori terbaik sekalipun akan kalah dengan praktik buruk yang dibiarkan melenggang ke production. Berikut adalah daftar dosa besar arsitektural yang wajib Anda hindari jika tidak ingin karir backend Anda diwarnai panggilan darurat tengah malam:

> [!WARNING]
> **Jangan Pernah Menyimpan State Global dalam Variabel Modulus.** 
> Menyimpan data sesi, cache query database, atau daftar pengguna aktif di dalam objek JavaScript biasa (`const cache = {}`) di dalam file modul Node.js adalah jalan pintas menuju bencana *memory leak*. Karena modul di-cache secara global setelah `require` pertama, objek tersebut tidak akan pernah dibersihkan oleh Garbage Collector sepanjang masa hidup proses aplikasi.

- **Menghindari Operasi Sinkron (`fs.readFileSync`, `crypto.pbkdf2Sync`) di Jalur HTTP Request:** Satu saja pemanggilan fungsi sinkron untuk membaca file konfigurasi atau hashing password di dalam *route handler* akan membekukan seluruh event loop. Ribuan request lain yang masuk akan tertahan (*starvation*), menyebabkan koneksi timeout massal di sisi Nginx.
- **Mengabaikan Penanganan Error pada Promise dan EventEmitter:** Mengabaikan *unhandledRejection* atau gagal mendengarkan event `'error'` pada *stream* dan *socket* akan menyebabkan aplikasi mengalami *silent failure* atau memicu crash mendadak yang merusak integritas data transaksi.
- **Membuat Listener secara Dinamis Tanpa Pembersihan:** Seringkali kita menambahkan `emitter.on('data', callback)` di dalam fungsi yang dipanggil berulang kali tanpa pernah memanggil `emitter.removeListener()`. Ini menciptakan *event emitter leak* di mana memori akan terus membengkak karena akumulasi referensi fungsi callback yang menumpuk di dalam heap.

Dengan memahami jalur bahaya ini, merancang konfigurasi yang tepat, dan menggunakan instrumen profiling seperti Clinic.js, kita tidak hanya membangun aplikasi Node.js yang cepat, tetapi juga infrastruktur backend yang kokoh, resilien, dan siap menghadapi skala enterprise apa pun.