Async Programming: Rust, Go, JavaScript — Siapa Paling Efisien?
Perbandingan async/await di Rust (tokio), Go (goroutines), dan JavaScript (event loop). Mana yang paling tepat buat use case lo?
Gue mulai ngoding backend dengan JavaScript. Async-nya? Callback hell. Bayangin nested callback 7 level buat satu flow transaksi — query user, validasi saldo, proses pembayaran, update database, kirim notif email, kirim notif push, log audit. Kodenya bentuk segitiga miring ke kanan. Lalu gue pindah ke Go dengan goroutines — rasanya kayak nge-hire 1000 pekerja mini yang jalan sendiri-sendiri. Terakhir Rust dengan Tokio — di sini gue belajar bahwa concurrent programming bisa zero-cost, tapi bayar mahal dengan compile time.
Kenapa Ini Penting — Tiga Model Concurrency, Tiga Filosofi
Tiga bahasa ini mewakili tiga pendekatan concurrency yang berbeda secara fundamental. JavaScript mengandalkan single-threaded event loop yang memaksa kamu berpikir async sejak hari pertama. Go menawarkan lightweight thread bikinan runtime — concurrency semudah nambah kata "go" di depan fungsi. Rust menghadirkan zero-cost futures yang dikompilasi jadi state machine, lengkap dengan borrow checker yang memastikan tidak ada data race. Masing-masing punya tempatnya. Artikel ini membedah ketiganya — bukan untuk mencari pemenang, tapi supaya kamu tahu kapan pakai yang mana.
JavaScript — Single Thread, Event Loop, dan Microtask
JavaScript berjalan di atas single thread. Satu call stack. Tapi kok bisa menangani ribuan request concurrent? Jawabannya: event loop. Ketika kamu memanggil fetch() atau setTimeout(), operasinya di-offload ke Web API (browser) atau libuv (Node.js). Begitu operasi selesai, callback-nya masuk ke queue. Event loop mengambil satu per satu dari queue — tapi dengan prioritas: microtask queue (Promise) lebih dulu daripada macrotask queue (setTimeout, setInterval).
Coba lihat kode ini:
Ini fundamental yang sering bikin bingung. Promise.resolve().then() masuk microtask queue — dikerjakan SETELAH call stack kosong, tapi SEBELUM macrotask berikutnya. setTimeout masuk macrotask — dikerjakan setelah SEMUA microtask selesai.
Untuk concurrency I/O, pattern async/await membuat kode async terlihat sync:
Tapi kelemahan terbesarnya: CPU-bound task membuat event loop macet. Kamu bikin while(true) di main thread? Seluruh server hang. Tidak ada request lain yang bisa ter-handle. Inilah kenapa Node.js biasanya tidak dipakai untuk heavy computation. Image processing, video encoding, atau ML inference di main thread adalah bunuh diri.
Single-threaded bukan berarti JavaScript tidak bisa concurrency. Worker threads ada — tapi isolated, tidak share memory. Harus message passing. Komunikasi antar thread overhead-nya tinggi. Untuk I/O-bound typical backend, event loop model justru optimal — tidak ada context switching overhead, tidak ada lock contention.
Go — Goroutines, Channel, dan "Don't Share Memory"
Go punya model concurrency yang radikal beda dari JavaScript. Kamu tidak perlu mikirin event loop. Tidak perlu mikirin microtask vs macrotask. Kamu tinggal tulis go di depan fungsi — dan itu sudah berjalan concurrent. Go runtime yang mengatur semuanya: work-stealing scheduler, GOMAXPROCS, dan growable stack.
Yang bikin Go spesial: goroutines bukan OS thread. Mereka "green thread" yang di-manage Go runtime. Kamu bisa spawn ratusan ribu goroutines tanpa bikin OS menangis. Golang scheduler pakai work-stealing — goroutine yang ready bisa di-steal oleh OS thread yang idle. Stack goroutine mulai dari 2KB dan bisa grow otomatis.
Channel adalah primitif komunikasi antar goroutine. Filosofi Go: "Don't communicate by sharing memory; share memory by communicating." Kamu kirim data lewat channel, bukan share variabel global pakai mutex. Ini kode concurrent HTTP fetch:
Channel ada yang buffered (bisa menampung n item sebelum sender block) dan unbuffered (sender + receiver harus ready bersamaan). Select untuk multiplex channel:
Kelemahan Go: goroutine scheduler ada overhead, GC masih ada (walaupun makin pendek, ~100μs di Go 1.24). Shared memory masih mungkin — kamu tetap bisa pakai sync.Mutex. Go tidak memaksa kamu aman; dia berharap kamu disiplin.
Rust — Zero-Cost Futures, Pin, dan Borrow Checker
Rust async adalah yang paling kompleks — tapi juga paling efisien. Di Rust, async fn tidak langsung berjalan. Dia mengembalikan Future — sebuah state machine yang hanya maju kalau di-poll. Kamu harus memilih runtime (Tokio biasanya) untuk men-drive future ini.
Kenapa ini powerful? Future di Rust adalah zero-cost abstraction. Compiler mentransformasi async fn menjadi enum state machine — tidak ada heap allocation kecuali kamu minta explicit (Box::pin). Tidak ada runtime overhead seperti goroutine scheduler. Tidak ada garbage collector. Future hanya berjalan kalau di-poll.
Tapi ini sumber kompleksitas. Rust butuh Pin karena future yang sedang berjalan tidak boleh dipindahkan memorinya. Send + Sync trait bounds memastikan kamu tidak share mutable state antar thread tanpa sinkronisasi. Borrow checker-nya tidak cuma compile-time — dia menjaga runtime juga. Ini contoh concurrent fetch pakai Tokio:
Kelemahan Rust: learning curve curam. Kamu harus ngerti Pin, Unpin, Send, Sync, Future, Poll, Waker — sebelum bikin HTTP server concurrent. Compile time lama. Ekosistem async Rust masih pecah — Tokio, async-std, smol, embassy. Satu kode async Tokio tidak langsung kompatibel dengan runtime lain.
Head-to-Head — Mana Lebih Cepat, Mana Lebih Mudah?
Untuk I/O-bound seperti HTTP server pada umumnya, perbedaan performanya tidak sedramatis yang dibayangkan. Tokio memang lebih cepat — benchmark Techempower menempatkan Rust di top 10, Go di top 20, dan Node.js di top 50. Tapi kenyataannya, untuk 99% use case, Go sudah cukup cepat dengan developer experience yang jauh lebih bersahabat.
Untuk CPU-bound, Rust adalah juaranya — zero-overhead, tidak ada yang menandingi efisiensinya. Go juga cukup bagus selama kamu set GOMAXPROCS > 1 agar bisa memanfaatkan multiple core. JavaScript? Jangan pernah jalankan CPU-bound di main thread, itu resep bencana.
Dari sisi memory: goroutine mulai dari 2KB dan bisa grow sesuai kebutuhan. Tokio task bahkan lebih ringan. Node.js memang single thread, tapi V8 heap bisa membengkak. Realitanya, Go dan Rust cukup sebanding untuk throughput, dengan Rust biasanya 10-30% lebih hemat CPU.
Dari sisi developer experience, Go adalah sweet spot. Kamu tidak perlu pusing memikirkan Pin atau Waker. Tulis go, pakai channel, langsung jalan. JavaScript juga mudah — async/await terasa natural. Rust? Kamu akan berantem sama compiler selama beberapa hari. Tapi begitu berhasil compile, kodenya hampir selalu berjalan benar.
Decision Framework — Kapan Pakai yang Mana?
Pilih Go kalau kamu bikin API server atau microservice. Ini sweet spot-nya. Concurrency simpel, deployment gampang (single binary), performa cukup. Tim gampang onboarding. Tidak perlu mikirin event loop atau Pin.
Pilih JavaScript (Node.js) kalau kamu bikin real-time app atau full-stack dengan tim JavaScript-heavy. WebSocket di Node.js masih paling mature. Atau kalau kamu prototyping — speed development Node.js susah dikalahkan.
Pilih Rust kalau kamu bikin database, proxy, atau sistem yang tidak boleh crash. Performa adalah fitur, bukan nice-to-have. Memory safety tanpa GC. Tapi siapkan waktu onboarding yang signifikan.
Gak Ada yang Universally Better
Gue sudah menulis production code di ketiga bahasa ini. Kesimpulannya: tidak ada yang universally better. JavaScript raja prototyping — full-stack app dalam sehari. Go raja maintainability — kode 2 tahun lalu masih gampang dibaca. Rust raja performa — throughput maksimal dengan safety guarantee.
Yang paling penting: pilih berdasarkan apa yang kamu bangun dan siapa yang maintain. Jangan pilih Rust karena keren. Jangan hindari JavaScript karena "single-threaded". Jangan underestimate Go — kesederhanaannya adalah fitur, bukan kekurangan.
Referensi
1. Tokio Tutorial — Async Rust
3. MDN — JavaScript Event Loop
