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 — 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 beda secara fundamental. JavaScript — single-threaded event loop yang maksa lo mikir async dari hari pertama. Go — lightweight thread bikinan runtime yang bikin concurrency semudah nambah kata "go" di depan fungsi. Rust — zero-cost futures yang dikompilasi jadi state machine, dengan borrow checker yang mastiin gak ada data race. Masing-masing punya tempatnya. Artikel ini bakal bedah ketiganya — bukan buat nyari pemenang, tapi buat lo tau kapan pake yang mana.
JavaScript — Single Thread, Event Loop, dan Microtask
JavaScript jalan di atas single thread. Satu call stack. Tapi kok bisa handle ribuan request concurrent? Jawabannya: event loop. Ketika lo panggil fetch() atau setTimeout(), operasinya di-offload ke Web API (browser) atau libuv (Node.js). Begitu operasi selesai, callback-nya masuk ke queue. Event loop ambil satu per satu dari queue — tapi dengan prioritas: microtask queue (Promise) lebih dulu dari macrotask queue (setTimeout, setInterval).
Coba liat kode ini:
Ini fundamental yang sering bikin bingung. Promise.resolve().then() masuk microtask queue — dikerjain SETELAH call stack kosong, tapi SEBELUM macrotask berikutnya. setTimeout masuk macrotask — dikerjain setelah SEMUA microtask selesai.
Untuk concurrency I/O, pattern async/await bikin kode async keliatan sync:
Tapi kelemahan terbesarnya: CPU-bound task bikin event loop macet. Lo bikin while(true) di main thread — seluruh server hang. Gak ada request lain yang bisa ke-handle. Ini kenapa Node.js biasanya gak dipake buat heavy computation. Image processing, video encoding, atau ML inference di main thread adalah bunuh diri.
Single-threaded bukan berarti JavaScript gak bisa concurrency. Worker threads ada — tapi isolated, gak share memory. Harus message passing. Komunikasi antar thread overhead-nya tinggi. Untuk I/O-bound typical backend, event loop model justru optimum — gak ada context switching overhead, gak ada lock contention.
Go — Goroutines, Channel, dan "Don't Share Memory"
Go punya model concurrency yang radikal beda dari JavaScript. Lo gak mikirin event loop. Lo gak mikirin microtask vs macrotask. Lo tinggal tulis go depan fungsi — dan itu udah jalan concurrent. Go runtime yang ngatur semuanya: work-stealing scheduler, GOMAXPROCS, dan growable stack.
Yang bikin Go spesial: goroutines bukan OS thread. Mereka "green thread" yang di-manage Go runtime. Lo bisa spawn ratusan ribu goroutines tanpa bikin OS nangis. Golang scheduler pakai work-stealing — goroutine yang ready bisa di-steal sama 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." Lo kirim data lewat channel, bukan share variabel global pake mutex. Ini kode concurrent HTTP fetch:
Channel ada yang buffered (bisa nampung n item sebelum sender block) dan unbuffered (sender + receiver harus ready barengan). Select untuk multiplex channel:
Kelemahan Go: goroutine scheduler ada overhead, GC masih ada (walaupun makin pendek, ~100us di Go 1.24). Shared memory masih mungkin — lo tetep bisa pake sync.Mutex. Go gak maksa lo aman; dia berharap lo disiplin.
Rust — Zero-Cost Futures, Pin, dan Borrow Checker
Rust async adalah yang paling kompleks — tapi juga paling efisien. Di Rust, async fn gak langsung jalan. Dia return Future — sebuah state machine yang cuma maju kalau di-poll. Lo harus pilih runtime (Tokio biasanya) buat nge-drive future ini.
Kenapa ini powerful? Future di Rust adalah zero-cost abstraction. Compiler transform async fn jadi enum state machine — gak ada heap allocation kecuali lo minta explicit (Box::pin). Gak ada runtime overhead kayak goroutine scheduler. Gak ada garbage collector. Future cuma jalan kalau di-poll.
Tapi ini sumber kompleksitas. Rust butuh Pin karena future yang lagi berjalan gak boleh dipindahin memorinya. Send + Sync trait bounds mastiin lo gak share mutable state antar thread tanpa sinkronisasi. Borrow checker-nya gak cuma compile-time — dia jaga runtime juga. Ini contoh concurrent fetch pake Tokio:
Kelemahan Rust: learning curve curam. Lo 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 gak langsung compatible sama runtime lain.
Head-to-Head — Mana Lebih Cepat, Mana Lebih Mudah?
Buat I/O-bound (HTTP server typical): perbedaannya gak sedramatis yang orang kira. Tokio lebih cepat — benchmark Techempower nunjukin Rust di top 10, Go di top 20, Node.js di top 50. Tapi untuk 99% use case, Go udah cukup cepat dengan developer experience jauh lebih baik.
Buat CPU-bound: Rust juara — zero-overhead. Go bagus dengan GOMAXPROCS > 1. JavaScript... jangan buat CPU-bound di main thread.
Dari sisi memory: goroutine mulai 2KB, bisa grow. Tokio task lebih ringan. Node.js single thread, tapi V8 heap bisa gede. Realitanya: Go dan Rust comparable untuk throughput, Rust biasanya 10-30% lebih hemat CPU.
Dari sisi DX: Go sweet spot. Lo gak mikirin Pin atau Waker. Tulis go, pake channel, jalan. JavaScript juga gampang — async/await natural. Rust... lo bakal berantem sama compiler beberapa hari. Tapi begitu compiled — hampir selalu jalan bener.
Decision Framework — Kapan Pake yang Mana?
Pilih Go kalau lo bikin API server atau microservice. Ini sweet spot-nya. Concurrency simpel, deployment gampang (single binary), performance cukup. Tim gampang onboard. Gak perlu mikirin event loop atau Pin.
Pilih JavaScript (Node.js) kalau lo bikin real-time app atau full-stack dengan tim JavaScript-heavy. WebSocket di Node.js masih paling mature. Atau kalau lo prototyping — speed development Node.js susah dikalahin.
Pilih Rust kalau lo bikin database, proxy, atau system yang gak boleh crash. Performance adalah fitur, bukan nice-to-have. Memory safety tanpa GC. Tapi siapin waktu onboarding yang signifikan.
Gak Ada yang Universally Better
Gue udah nulis production code di ketiga bahasa ini. Kesimpulan: gak ada yang universally better. JavaScript raja prototyping — full-stack app dalam sehari. Go raja maintainability — kode 2 tahun lalu masih gampang dibaca. Rust raja performance — throughput maksimal dengan safety guarantee.
Yang paling penting: pilih berdasarkan apa yang lo bangun dan siapa yang maintain. Jangan pilih Rust karena keren. Jangan hindarin JavaScript karena "single-threaded". Jangan underestimate Go — kesederhanaannya adalah fitur, bukan kekurangan.
Referensi
1. Tokio Tutorial — Async Rust
3. MDN — JavaScript Event Loop
