Bedanya CJS vs ESM di Node.js: Panduan Simpel Buat Kamu
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.
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¶
- 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.
- Simplicity (Kesederhanaan): Sintaks
require()danmodule.exportsitu relatif gampang dipahami, apalagi buat pemula di Node.js. - 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.
- Dynamic Require: Kamu bisa memanggil
require()secara kondisional di dalam fungsi atau blokif. Ini memberimu fleksibilitas dalam memuat modul berdasarkan kondisi tertentu saat runtime.
Kekurangan CJS¶
- 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.
- No Native Browser Support: CJS bukan standar JavaScript, jadi browser enggak mengenali sintaks
require/exportssecara langsung. Kamu butuh bundler (seperti Webpack, Browserify) buat mengubah kode CJS jadi format yang dimengerti browser. - 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. - Circular Dependencies: CJS punya cara penanganan circular dependencies (di mana Modul A
requireModul B, dan Modul BrequireModul A) yang bisa tricky dan kadang menghasilkan nilai yang enggak sesuai harapan karena modul dieksekusi secara parsial saat terjadi siklus.
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¶
- Native Standard (Standar Asli): ESM adalah bagian dari standar JavaScript, didukung langsung oleh browser modern dan Node.js versi baru.
- Asynchronous Loading (Pemuatan Asinkron): Ideal buat browser karena enggak blokir proses lain saat memuat skrip dari jaringan.
- Static Analysis Friendly: Karena struktur
importdanexportitu statis (tidak bisa diubah saat runtime sepertirequire), alat static analysis (linter, bundler, tree shakers) bisa bekerja lebih efektif. - 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.
- Flexible Syntax: ESM punya sintaks yang lebih beragam (
default export,named export,namespace import, re-exporting). - Dynamic Import (
import()): Meskipun default import bersifat statis, ESM juga punya cara buat memuat modul secara dinamis pakaiimport()(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¶
- 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"dipackage.json). - File Extensions in Browser: Di browser, kamu harus menyertakan ekstensi file (
.js,.mjs) dalam pernyataanimport, yang mungkin terasa kurang nyaman dibanding CJS di Node.js yang bisa menghilangkan ekstensi. Bundler biasanya mengatasi ini. - Browser Compatibility (Old): Browser lama tentu enggak mendukung ESM. Tapi ini makin jarang jadi masalah karena adopsi browser modern sudah sangat luas.
- Circular Dependencies Handling: Penanganan circular dependencies di ESM sedikit berbeda dan bisa terasa lebih ketat atau menghasilkan
undefinedjika tidak hati-hati mengatur alur eksekusi.
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:
- CJS
require()ESM: Ini tidak bisa dilakukan secara langsung dengan sintaksrequire(). 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 menggunakanimport()secara dinamis di dalam modul CJS, meskipun ini mengembalikan Promise, jadi tidak sesederhanaconst result = require('esm-module');. - ESM
importCJS: 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 pakaiimport cjsModule from 'commonjs-package';di file.mjsatau file.jsdengan"type": "module".
Mengaktifkan ESM di Node.js¶
Secara default, Node.js memperlakukan file .js sebagai CJS. Untuk mengaktifkan ESM, ada dua cara utama:
- Menggunakan ekstensi
.mjs: Node.js akan secara otomatis memperlakukan file dengan ekstensi.mjssebagai modul ESM. - Menggunakan
"type": "module"dipackage.json: Tambahkan"type": "module"ke dalam filepackage.jsondi root proyek kamu. Ini akan membuat Node.js memperlakukan semua file.jsdi dalam proyek tersebut (kecuali di dalamnode_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 pernyataanrequire()menjadiimport ... from ...danmodule.exports/exportsmenjadiexport .... - Perhatikan
__dirnamedan__filename: Di modul ESM, variabel global CJS seperti__dirnamedan__filenametidak tersedia. Kamu bisa mendapatkannya dengan menggunakan modulurldanpath:
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"dipackage.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
adddari modulutilstadi, fungsisubtractenggak akan ikut masuk ke bundle akhir. Ini beda samarequiredi 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