Pada aplikasi Nuxt 3 berskala produksi, worker Node.js kerap mengalami crash akibat Out-Of-Memory (OOM, exit code 137). Gejala umumnya terlihat dari metrik memory (RSS dan Heap Used) yang meningkat linear setiap kali server route menerima beban trafik, dan V8 Garbage Collector (GC) gagal mereklamasi alokasi tersebut bahkan saat trafik kembali normal.

Akar Masalah: Retensi Lifecycle H3Event di Module Scope

Server engine Nuxt 3 (Nitro) mengompilasi setiap file di direktori server/api/ atau server/routes/ menjadi modul ES yang dievaluasi satu kali saat proses server melakukan bootstrapping. Deklarasi objek di luar defineEventHandler berada pada module scope yang bersifat persisten sepanjang umur worker process.

Kebocoran memori terjadi ketika developer mendefinisikan cache in-memory (seperti Map atau objek global) pada module scope, lalu menyimpan data yang mengikat objek event (instance dari H3Event), fungsi handler, atau callback closure yang menangkap scope request. Akibatnya, rantai referensi GC root menahan H3Event, yang di dalamnya mempertahankan seluruh siklus hidup IncomingMessage, ServerResponse, dan event.context.

Contoh Pola Kode Bermasalah (Antipattern)

// server/api/report.ts
import { defineEventHandler } from 'h3';

// Module-scoped cache: Tetap hidup di memory proses selama worker berjalan
const requestCache = new Map<string, any>();

export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id') || 'default';

  if (!requestCache.has(id)) {
    // BENCANA GC: Closure di bawah mempertahankan referensi ke instance `event`
    requestCache.set(id, {
      timestamp: Date.now(),
      fetcher: () => {
        // Referensi ke `event` menyebabkan seluruh HTTP context tidak bisa di-collect
        return `Processed request for ${event.node.req.url}`;
      }
    });
  }

  const item = requestCache.get(id);
  return { message: item.fetcher() };
});

Reproduksi dan Profiling Heap Snapshot

Untuk membuktikan kebocoran memori secara presisi, jalankan aplikasi mode produksi dengan flag inspeksi V8.

1. Jalankan Build dengan Node.js Inspector

Bangun aplikasi terlebih dahulu, kemudian jalankan server output dengan Node.js inspector aktif:

npm run build
NODE_OPTIONS="--inspect=0.0.0.0:9229" node .output/server/index.mjs

2. Trigger Alokasi Menggunakan Autocannon

Gunakan autocannon untuk memberikan beban konkurensi pada endpoint server route:

npx autocannon -c 50 -d 30s -m GET "http://localhost:3000/api/report"

3. Analisis Retainer Tree di Chrome DevTools

  1. Buka browser Chromium dan akses chrome://inspect.
  2. Pilih target remote inspeksi Nitro Anda, buka tab Memory, lalu pilih Take Snapshot.
  3. Jalankan beban trafik via autocannon, lalu ambil snapshot kedua.
  4. Ubah view inspeksi dari Summary menjadi Comparison.
  5. Cari constructor H3Event atau NodeHTTPContext pada kolom pencarian Class filter.

Perhatikan kolom Distance dan Retained Size. Pada Retainer Tree di panel bawah, Anda akan melihat referensi objek bergerak menuju variabel requestCache di dalam konteks modul ES. V8 GC menganggap H3Event masih reachable, sehingga mencegah alokasi dibebaskan.

Langkah Perbaikan

Terdapat dua strategi standar industri untuk mengatasi state leak di server route Nitro: isolasi konteks per-request dan abstraksi storage terkelola.

Solusi 1: Isolasi State Per-Request Menggunakan event.context

Jika data hanya dibutuhkan sepanjang siklus satu request, simpan langsung pada event.context. Objek ini otomatis dibuang dari memori saat siklus HTTP request berakhir.

// server/middleware/user-context.ts
export default defineEventHandler((event) => {
  // Bersih: event.context terisolasi per lifecycle request
  event.context.executionProfile = {
    startTime: performance.now(),
    requestId: crypto.randomUUID()
  };
});

Solusi 2: Gunakan Nitro Storage (useStorage) untuk Global Caching

Jika data perlu diakses lintas request, jangan simpan instance objek kompleks, koneksi aktif, atau closure fungsi. Simpan hanya serializable data (JSON murni) dan gunakan useStorage() milik Nitro.

// server/api/report.ts
import { defineEventHandler, getRouterParam } from 'h3';

// ponytail: In-memory memory storage default, ganti mount redis di nuxt.config.ts saat multi-instance
export default defineEventHandler(async (event) => {
  const id = getRouterParam(event, 'id') || 'default';
  const storage = useStorage('cache');
  const cacheKey = `report:${id}`;

  const cached = await storage.getItem<{ timestamp: number; url: string }>(cacheKey);
  if (cached) {
    return { message: `Processed request for ${cached.url}`, cached: true };
  }

  // Hanya simpan data primitif atau serializable, BUKAN closure atau objek event
  const payload = {
    timestamp: Date.now(),
    url: event.node.req.url || ''
  };

  await storage.setItem(cacheKey, payload, { ttl: 60 });
  return { message: `Processed request for ${payload.url}`, cached: false };
});
Catatan Desain: useStorage() Nitro mengisolasi state ke driver penyimpanan (memory, redis, fs) secara aman tanpa risiko menahan scope closure dari fungsi handler.

Verifikasi Pasca-Perbaikan

Ulangi pengujian beban dengan konfigurasi yang identik:

npx autocannon -c 100 -d 60s "http://localhost:3000/api/report"

Ambil Heap Snapshot ketiga setelah eksekusi selesai. Evaluasi metrik:

  • Heap Growth Plateau: Ukuran heap mencapai level stabil dan membentuk kurva gergaji (sawtooth pattern) yang menandakan V8 GC berhasil membersihkan alokasi memori secara periodik.
  • Instance Count: Jumlah instance H3Event dan ServerResponse turun mendekati angka 0 sesaat setelah pengujian beban dihentikan.