Bedanya CJS vs ESM di Node.js: Panduan Simpel Buat Kamu

Table of Contents

Dunia JavaScript itu dinamis banget, ada aja fitur baru atau cara baru buat ngoding. Salah satu area yang cukup bikin “rame” dan sering ditanyain adalah soal sistem modul. Dulu, JavaScript di browser tuh enggak punya sistem modul bawaan. Kita harus akalin pakai global variables atau pola desain kayak IIFE (Immediately Invoked Function Expression) biar kode enggak tabrakan satu sama lain. Nah, di lingkungan server kayak Node.js, muncul solusi dari komunitas, yang paling populer namanya CommonJS (CJS). Belakangan, JavaScript secara resmi punya standar modul sendiri yang disebut ECMAScript Modules (ESM).

Sekarang, pertanyaannya: apa bedanya CJS dan ESM? Terus, mana yang sebaiknya kita pakai? Yuk, kita bongkar satu per satu.

Perbedaan CJS dan ESM
Image just for illustration

CJS: Si Legendaris (CommonJS)

CommonJS itu bisa dibilang pionirnya sistem modul di luar browser, terutama di Node.js. Sebelum ESM populer, CJS adalah standar de facto buat ngatur dependensi dan organisasi kode di ekosistem Node. CJS didesain buat lingkungan server-side, makanya punya karakteristik yang beda sama ESM.

Salah satu ciri khas CJS adalah cara loading modulnya yang sinkron. Artinya, ketika kamu pakai require(), Node.js akan langsung cari, eksekusi, dan kembalikan exports dari modul tersebut sebelum melanjutkan baris kode berikutnya. Ini cocok banget buat skenario server di mana file-file lokal bisa diakses dengan cepat.

Cara Kerja CJS

Di CJS, ada dua hal utama yang perlu kamu tahu: require() buat mengimpor modul dan module.exports atau exports buat mengekspor sesuatu dari modul kamu.

Misalnya, kamu punya file utils.js kayak gini:

// utils.js
function add(a, b) {
  return a + b;
}

function subtract(a, b) {
  return a - b;
}

module.exports = {
  add: add,
  subtract: subtract
};

// Atau bisa juga pakai 'exports' sebagai shortcut, tapi hati-hati:
// exports.add = add;
// exports.subtract = subtract;
// Jangan langsung assign ke exports, misalnya exports = { ... }, karena exports cuma referensi ke module.exports

Terus, di file lain, misalnya app.js, kamu mau pakai fungsi add dan subtract:

// app.js
const utils = require('./utils'); // Loading sinkron terjadi di sini

console.log(utils.add(5, 3));    // Output: 8
console.log(utils.subtract(10, 4)); // Output: 6

Ketika require('./utils') dipanggil, Node.js akan mencari file utils.js, menjalankannya, dan menangkap nilai dari module.exports. Nilai ini kemudian di-cache, jadi kalau kamu require modul yang sama lagi, Node.js tinggal ambil dari cache tanpa perlu eksekusi ulang. Ini salah satu alasan kenapa CJS cepat di lingkungan Node.js.

Kelebihan CJS

  1. Maturity (Kematangan): Ekosistem Node.js dibangun di atas CJS. Banyak banget package di npm yang awalnya ditulis pakai CJS. Jadi, kalau kamu kerja dengan proyek Node.js yang sudah ada atau menggunakan banyak library lama, CJS akan sangat familiar.
  2. Simplicity (Kesederhanaan): Sintaks require() dan module.exports itu relatif gampang dipahami, apalagi buat pemula di Node.js.
  3. Synchronous Loading (Pemuatan Sinkron): Di lingkungan server, ini justru bisa jadi kelebihan karena file lokal bisa diakses cepat. Kode jadi lebih linear dan mudah diikuti alurnya.
  4. Dynamic Require: Kamu bisa memanggil require() secara kondisional di dalam fungsi atau blok if. Ini memberimu fleksibilitas dalam memuat modul berdasarkan kondisi tertentu saat runtime.

Kekurangan CJS

  1. Synchronous Loading in Browsers: Nah, ini problem utamanya kalau mau pakai CJS di browser. Memuat modul secara sinkron di web itu buruk buat performa. Browser harus berhenti parsing HTML/CSS/JS sampai modul selesai di-load. Makanya CJS enggak cocok buat browser tanpa bantuan bundler.
  2. No Native Browser Support: CJS bukan standar JavaScript, jadi browser enggak mengenali sintaks require/exports secara langsung. Kamu butuh bundler (seperti Webpack, Browserify) buat mengubah kode CJS jadi format yang dimengerti browser.
  3. Limited Static Analysis: Karena require() bisa dipanggil secara dinamis, alat static analysis (seperti yang dipakai buat tree shaking) kesulitan menentukan dependensi sebuah modul sebelum kode dieksekusi. Ini bikin optimasi kode lebih susah.
  4. Circular Dependencies: CJS punya cara penanganan circular dependencies (di mana Modul A require Modul B, dan Modul B require Modul A) yang bisa tricky dan kadang menghasilkan nilai yang enggak sesuai harapan karena modul dieksekusi secara parsial saat terjadi siklus.

CommonJS example
Image just for illustration

ESM: Sang Standar Baru (ECMAScript Modules)

ESM adalah sistem modul resmi yang diperkenalkan di ECMAScript 2015 (ES6). Tujuannya adalah menyediakan standar modul yang bisa dipakai di mana saja, baik di browser maupun di lingkungan server seperti Node.js. ESM didesain dengan mempertimbangkan sifat web yang asinkron.

Beda sama CJS yang loading-nya sinkron, ESM didesain buat loading modul secara asinkron. Ini sangat penting buat browser karena browser bisa terus melakukan hal lain (seperti merender halaman) sambil menunggu file JavaScript di-download.

Cara Kerja ESM

Di ESM, kita pakai kata kunci import buat mengimpor modul dan export buat mengekspor sesuatu. Sintaksnya juga lebih fleksibel.

Misalnya, file utils.js tadi kalau ditulis pakai ESM jadi gini:

// utils.js (menggunakan ESM)
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

// Bisa juga ekspor default:
// const multiply = (a, b) => a * b;
// export default multiply;

Terus, di file lain, misalnya app.js, kamu mau pakai fungsi add dan subtract:

// app.js (menggunakan ESM)
import { add, subtract } from './utils.js'; // Loading asinkron

console.log(add(5, 3));    // Output: 8
console.log(subtract(10, 4)); // Output: 6

// Kalau ada default export:
// import multiply from './utils.js';
// console.log(multiply(2, 3)); // Output: 6

// Import semua yang diekspor:
// import * as utils from './utils.js';
// console.log(utils.add(5, 3));

Perhatikan beberapa hal:
- Kita pakai import dan export.
- Kita bisa mengimpor nama spesifik ({ add, subtract }) atau semuanya (* as utils).
- Di browser, kalau kamu pakai <script type="module">, kamu perlu pakai ekstensi file (./utils.js) di pernyataan import. Node.js modern juga merekomendasikan ini, meskipun kadang bisa diatur tanpa ekstensi tergantung konfigurasi.

Proses loading modul di ESM itu dibagi jadi beberapa fase:
1. Construction (Konstruksi): Parser mencari semua pernyataan import dan membuat “peta” ketergantungan antar modul. Ini dilakukan sebelum kode dieksekusi (static analysis).
2. Loading (Memuat): Fetcher mengambil file modul dari network atau disk.
3. Linking (Menautkan): Modul dihubungkan satu sama lain berdasarkan peta ketergantungan tadi, mengekspos exports dari setiap modul.
4. Evaluation (Evaluasi): Kode di dalam modul dieksekusi.

Karena construction dan linking dilakukan sebelum eksekusi dan bersifat statis, ESM sangat ideal buat static analysis dan optimasi seperti tree shaking.

Kelebihan ESM

  1. Native Standard (Standar Asli): ESM adalah bagian dari standar JavaScript, didukung langsung oleh browser modern dan Node.js versi baru.
  2. Asynchronous Loading (Pemuatan Asinkron): Ideal buat browser karena enggak blokir proses lain saat memuat skrip dari jaringan.
  3. Static Analysis Friendly: Karena struktur import dan export itu statis (tidak bisa diubah saat runtime seperti require), alat static analysis (linter, bundler, tree shakers) bisa bekerja lebih efektif.
  4. Tree Shaking Support: Kemampuan static analysis ini memungkinkan bundler menghapus kode yang tidak terpakai (dead code elimination), yang sering disebut tree shaking. Ini bikin ukuran bundle akhir jadi lebih kecil.
  5. Flexible Syntax: ESM punya sintaks yang lebih beragam (default export, named export, namespace import, re-exporting).
  6. Dynamic Import (import()): Meskipun default import bersifat statis, ESM juga punya cara buat memuat modul secara dinamis pakai import() (perhatikan kurungnya). Ini mengembalikan Promise, jadi sifatnya asinkron dan bisa dipakai buat code splitting.

// Contoh dynamic import() di ESM
button.addEventListener('click', async () => {
  const module = await import('./heavy-module.js'); // Modul baru dimuat pas tombol diklik
  module.doSomething();
});

Ini sangat berguna buat memecah kode (code splitting) agar aplikasi web awal yang dimuat lebih cepat, dan bagian lain dimuat belakangan sesuai kebutuhan pengguna.

Kekurangan ESM

  1. Adoption Challenges: Node.js butuh waktu buat mengadopsi ESM sepenuhnya dan memastikan kompatibilitas dengan ekosistem CJS yang sudah ada. Ada konfigurasi tambahan yang perlu diperhatikan ("type": "module" di package.json).
  2. File Extensions in Browser: Di browser, kamu harus menyertakan ekstensi file (.js, .mjs) dalam pernyataan import, yang mungkin terasa kurang nyaman dibanding CJS di Node.js yang bisa menghilangkan ekstensi. Bundler biasanya mengatasi ini.
  3. Browser Compatibility (Old): Browser lama tentu enggak mendukung ESM. Tapi ini makin jarang jadi masalah karena adopsi browser modern sudah sangat luas.
  4. Circular Dependencies Handling: Penanganan circular dependencies di ESM sedikit berbeda dan bisa terasa lebih ketat atau menghasilkan undefined jika tidak hati-hati mengatur alur eksekusi.

ESM import export
Image just for illustration

Head-to-Head: Perbandingan Langsung

Supaya lebih jelas, ini tabel perbandingan antara CJS dan ESM:

Fitur CommonJS (CJS) ECMAScript Modules (ESM)
Sintaks Import require('modul') import ... from 'modul'
Sintaks Export module.exports = ... atau exports.name = ... export ... atau export default ...
Tipe Loading Sinkron (Synchronous) Asinkron (Asynchronous)
Native Support Node.js (utama), tidak di browser Browser (via <script type="module">), Node.js (via konfigurasi)
Static Analysis Terbatas (karena require dinamis) Sangat baik
Tree Shaking Sulit/Tidak didukung langsung Sangat didukung
Dynamic Import Bisa pakai require() (sinkron) Bisa pakai import() (asinkron, Promise-based)
Use Case Utama Server-side (Node.js lama), bundler Browser (web), Server-side (Node.js baru), Library/Framework modern

Berikut adalah visualisasi sederhana perbedaan loading antara CJS dan ESM:

```mermaid
graph TD
A[Start] → B{Modul A butuh Modul B?};
B – Yes → C[Load Modul B];
C – CJS (Synchronous) → D[Eksekusi Modul B];
D → E[Lanjutkan Eksekusi Modul A];
B – Yes → F[Load Modul B];
F – ESM (Asynchronous) → G[Sementara itu, lakukan hal lain];
F – Selesai Load → H[Link Modul B];
H → I[Eksekusi Modul B];
I → J[Lanjutkan Eksekusi Modul A];

B -- No --> J;

```
Diagram di atas menunjukkan bahwa CJS menunggu Modul B selesai dieksekusi sebelum melanjutkan Modul A, sementara ESM bisa “melakukan hal lain” (misalnya memuat modul lain atau parsing) sambil menunggu Modul B selesai dimuat dari jaringan.

Migrasi dan Interoperabilitas

Karena CJS dan ESM sama-sama eksis, terutama di Node.js, seringkali kita perlu cara buat keduanya bisa “ngobrol”. Node.js punya mekanisme bawaan untuk ini:

  1. CJS require() ESM: Ini tidak bisa dilakukan secara langsung dengan sintaks require(). Modul CJS tidak bisa langsung me-require modul ESM karena ESM punya model loading dan execution yang berbeda (statis vs dinamis, asinkron vs sinkron). Namun, kamu bisa menggunakan import() secara dinamis di dalam modul CJS, meskipun ini mengembalikan Promise, jadi tidak sesederhana const result = require('esm-module');.
  2. ESM import CJS: Ini bisa dilakukan di Node.js versi modern. Ketika modul ESM meng-import modul CJS, Node.js akan “membungkus” modul CJS tersebut agar bisa diekspor dan di-import oleh modul ESM. Jadi, kamu bisa pakai import cjsModule from 'commonjs-package'; di file .mjs atau file .js dengan "type": "module".

Mengaktifkan ESM di Node.js

Secara default, Node.js memperlakukan file .js sebagai CJS. Untuk mengaktifkan ESM, ada dua cara utama:

  1. Menggunakan ekstensi .mjs: Node.js akan secara otomatis memperlakukan file dengan ekstensi .mjs sebagai modul ESM.
  2. Menggunakan "type": "module" di package.json: Tambahkan "type": "module" ke dalam file package.json di root proyek kamu. Ini akan membuat Node.js memperlakukan semua file .js di dalam proyek tersebut (kecuali di dalam node_modules) sebagai modul ESM. Kalau kamu masih punya file CJS, kamu perlu mengubah ekstensinya jadi .cjs.
// package.json
{
  "name": "my-project",
  "version": "1.0.0",
  "type": "module", // Ini yang bikin Node.js pakai ESM
  ...
}

Memilih salah satu cara ini tergantung preferensi dan kompleksitas proyek kamu. Untuk proyek baru, menggunakan "type": "module" dan menulis semua kode dalam ESM seringkali merupakan pendekatan yang lebih bersih.

Tips Migrasi dari CJS ke ESM

  • Ganti sintaks require/exports: Ubah semua pernyataan require() menjadi import ... from ... dan module.exports/exports menjadi export ....
  • Perhatikan __dirname dan __filename: Di modul ESM, variabel global CJS seperti __dirname dan __filename tidak tersedia. Kamu bisa mendapatkannya dengan menggunakan modul url dan path:
    import { fileURLToPath } from 'url';
    import { dirname } from 'path';
    
    const __filename = fileURLToPath(import.meta.url);
    const __dirname = dirname(__filename);
    
  • Cek dependencies: Pastikan library yang kamu pakai sudah mendukung ESM atau setidaknya bisa di-import oleh modul ESM di Node.js. Mayoritas library modern sudah mendukung ini.
  • Konfigurasi bundler: Jika kamu menggunakan bundler (untuk browser atau Node.js), pastikan konfigurasinya sudah sesuai untuk menangani ESM.

Kapan Menggunakan CJS vs. ESM?

  • Untuk Proyek Baru: Sebaiknya gunakan ESM. Ini adalah standar masa depan, didukung browser dan Node.js modern, punya fitur yang lebih baik (static analysis, tree shaking, dynamic import), dan sintaksnya lebih modern.
  • Untuk Proyek Node.js Lama: Kalau kamu bekerja di proyek Node.js yang sudah besar dan berbasis CJS, migrasi total mungkin memakan waktu dan usaha. Kamu bisa tetap menggunakan CJS atau perlahan-lahan migrasi per modul. Node.js memungkinkan keduanya hidup berdampingan.
  • Untuk Development Tools/Skrip Internal: Terkadang, skrip utilitas atau build script di proyek Node.js masih sering ditulis dalam CJS karena tooling-nya (seperti Webpack config files versi lama, atau beberapa skrip npm) mungkin masih mengandalkan CJS. Tapi trennya juga sudah bergerak ke arah mendukung ESM.
  • Untuk Library yang Akan Dipublikasikan ke npm: Idealnya, library modern harus menyediakan build dalam format ESM (biasanya diatur di field "exports" atau "module" di package.json) agar bisa dimanfaatkan fitur-fitur ESM oleh bundler atau konsumen library tersebut. Menyediakan build CJS (di field "main") juga penting untuk kompatibilitas mundur.

Fakta Menarik Seputar Modul JavaScript

  • Sebelum CJS dan ESM, ada juga sistem modul lain yang populer seperti AMD (Asynchronous Module Definition), terutama dipakai sama RequireJS buat browser, dan UMD (Universal Module Definition) yang coba menggabungkan CJS dan AMD biar bisa dipakai di mana-mana.
  • Butuh waktu bertahun-tahun buat ESM jadi standar yang mapan. Perdebatan dan implementasinya di berbagai lingkungan (browser, Node.js) cukup panjang. Node.js sendiri awalnya cukup hati-hati dalam mengadopsi ESM biar enggak merusak ekosistem CJS yang sudah ada.
  • Fitur tree shaking di ESM sangat powerful buat mengurangi ukuran bundle JavaScript di aplikasi web modern. Dengan tree shaking, bundler cuma ngambil kode dari modul yang benar-benar kamu pakai. Misalnya, kalau kamu cuma import fungsi add dari modul utils tadi, fungsi subtract enggak akan ikut masuk ke bundle akhir. Ini beda sama require di CJS yang cenderung me-load seluruh modul.

ESM jelas merupakan masa depan modul JavaScript, menawarkan performa yang lebih baik, kemampuan optimasi yang lebih kuat, dan menjadi standar universal. Namun, CJS masih relevan, terutama di ekosistem Node.js yang sudah matang. Memahami keduanya penting agar kamu bisa memilih alat yang tepat untuk pekerjaan yang tepat, atau bekerja secara efektif di proyek yang menggunakan salah satunya, atau bahkan keduanya secara bersamaan.

Bagaimana pengalamanmu dengan CJS dan ESM? Pernah migrasi dari CJS ke ESM? Atau punya tips lain? Yuk, share di kolom komentar!

Posting Komentar