Seorang kasir melayani antrean dengan satu aturan sederhana: satu pesanan diselesaikan sampai tuntas sebelum pelanggan berikutnya dilayani. Aturan ini adil selama setiap pesanan cepat. Masalah muncul ketika Budi, kasir sekaligus pemilik kafe, diminta menghitung laporan penjualan mingguan di tengah jam sibuk — selama perhitungan berlangsung, seluruh antrean berhenti total.

JavaScript berjalan dengan aturan kasir yang sama. Runtime hanya punya satu thread dan satu call stack, sehingga satu tugas harus tuntas sebelum tugas berikutnya dimulai. Chapter ini membedah akibat dari aturan itu — operasi lambat yang menahan semuanya (blocking) — lalu menunjukkan jalan keluar paling dasarnya: mendaftarkan pekerjaan ke runtime dan membiarkan thread terus melayani kode lain. Pemahaman ini menjadi fondasi seluruh section Asynchronous JavaScript.

Satu Thread untuk Semua Kode #

JavaScript, baik di browser maupun di Node.js, adalah bahasa single-threaded: runtime mengeksekusi satu perintah dalam satu waktu melalui satu call stack. Call stack adalah tumpukan yang mencatat function mana yang sedang berjalan — setiap pemanggilan function menumpuk satu bingkai di atas, dan setiap function yang selesai mengangkat bingkainya. Selama call stack tidak kosong, runtime tidak bisa mengerjakan hal lain di luar tugas yang sedang berjalan.

Konsekuensinya terasa di dua tempat berbeda. Di browser, thread yang sama juga mengerjakan rendering halaman, sehingga kode yang berjalan lama membuat tombol tidak merespons klik dan animasi membeku. Di Node.js, permintaan pelanggan lain harus menunggu sampai tugas yang sedang berjalan selesai. Karena itu kita membagi operasi menjadi dua jenis:

  • Blocking: operasi yang menahan thread sampai selesai, misalnya loop perhitungan berat. Tidak ada kode lain yang bisa jalan di sela-selanya.
  • Non-blocking: operasi yang hasilnya menyusul nanti, sementara thread melanjutkan pekerjaan lain, misalnya timer. Pola ini yang menjadi dasar seluruh kode asinkron.

Mekanisme call stack bisa dilacak langkah demi langkah pada kode sederhana. Ketika function hitungLaporan dipanggil, bingkai function itu ditaruh di puncak tumpukan; ketika loop di dalamnya berputar, tumpukan itu masih penuh; dan hanya setelah return dijalankan, bingkainya diangkat dan baris berikutnya boleh dieksekusi. Tidak ada mekanisme untuk menghentikan sementara function yang sedang berjalan — satu-satunya cara thread bebas adalah function tersebut benar-benar selesai.

Blocking: Ketika Satu Tugas Menahan Semuanya #

Kita buktikan dulu perilaku blocking dengan berkas berikut. Function hitungLaporan sengaja menyibukkan thread selama 30 milidetik menggunakan loop kosong — simulasi perhitungan berat tanpa library tambahan. Skenario kedua menunjukkan pesanan yang didelegasikan ke mesin kopi:

// blocking-vs-nonblocking.js
// Simulasi blocking vs non-blocking di satu thread.

// --- 1. pekerjaan berat: loop sengaja menyibukkan thread ---
function hitungLaporan(durasiMs) {
  const mulai = Date.now();
  while (Date.now() - mulai < durasiMs) {
    // loop kosong: mensimulasikan perhitungan yang memakan waktu
  }
  return "laporan penjualan selesai dihitung";
}

// --- 2. skenario blocking: kasir menghitung laporan sendiri ---
console.log("A. Budi melayani pesanan pertama");
console.log("B. Budi mulai menghitung laporan...");
const hasilLaporan = hitungLaporan(30);
console.log("C.", hasilLaporan);
console.log("D. pesanan kedua dilayani setelah laporan selesai");

// --- 3. skenario non-blocking: seduhan didelegasikan ke mesin kopi ---
console.log("E. pesanan kopi Budi diterima");
setTimeout(() => {
  console.log("G. mesin kopi selesai, pesanan diantar");
}, 30);
console.log("F. Budi melayani pelanggan berikutnya");

Output yang Dihasilkan:

text
A. Budi melayani pesanan pertama
B. Budi mulai menghitung laporan...
C. laporan penjualan selesai dihitung
D. pesanan kedua dilayani setelah laporan selesai
E. pesanan kopi Budi diterima
F. Budi melayani pelanggan berikutnya
G. mesin kopi selesai, pesanan diantar

Mari kita telaah baris-baris pentingnya:

  1. Baris 5–11 (hitungLaporan): loop while memanggil Date.now() berulang-ulang dan baru berhenti setelah 30 milidetik lewat. Selama loop berputar, call stack penuh oleh function ini.
  2. Baris 14–15 (log A dan B): dua log mencetak seketika karena keduanya kode sinkron biasa.
  3. Baris 16 (const hasilLaporan = hitungLaporan(30)): inilah titik blocking. Pemanggilan function menahan call stack sampai loop selesai; log C dan D pada baris 17–18 wajib menunggu.
  4. Baris 21–24 (setTimeout): callback pesanan kopi tidak dieksekusi di sini. Runtime hanya mendaftarkannya dengan jeda 30 milidetik, lalu eksekusi langsung melanjutkan.
  5. Baris 25 (log F): tercetak sebelum G, meskipun di kode G ditulis lebih dulu — bukti bahwa thread tidak menunggu mesin kopi.

Bukti Tambahan: Timer Terlambat Karena Blocking #

Kalau timer benar-benar tidak menahan thread, apa yang terjadi saat thread sedang disibukkan? Timer yang diminta 10 milidetik harus mengantri sampai thread bebas. Cuplikan berikut membuktikannya:

js
function sibuk(durasiMs) {
  const mulai = Date.now();
  while (Date.now() - mulai < durasiMs) {}
}

const mulai = Date.now();
setTimeout(() => {
  const jedaNyata = Date.now() - mulai;
  console.log("jeda yang diminta: 10 ms");
  console.log("callback terlambat:", jedaNyata >= 30);
}, 10);
sibuk(30);

Output yang Dihasilkan:

text
jeda yang diminta: 10 ms
callback terlambat: true

setTimeout diminta 10 milidetik, tetapi callback baru berjalan setelah 30 milidetik atau lebih — setelah loop selesai. Argument jedaNyata >= 30 bernilai true secara pasti karena callback tidak mungkin berjalan sebelum kode sinkron mengosongkan thread. Dari sini kita bisa menarik kesimpulan yang sama pentingnya dengan cara membuat timer: delay adalah waktu minimum, dan antrean menentukan sisanya.

Non-blocking: Mendaftarkan Pekerjaan, Bukan Menunggu #

Urutan keluaran E → F → G adalah inti chapter ini. Baris 22–24 tidak menunda program; ia menitipkan callback ke runtime beserta jeda waktu, lalu kendali segera kembali ke baris berikutnya. Thread memakai sela itu untuk mencetak F. Barulah setelah 30 milidetik, callback G mendapat giliran dan dijalankan oleh runtime.

Inilah perbedaan mendasar kedua skenario: pada skenario pertama Budi menghitung laporan sendiri sehingga antrean berhenti; pada skenario kedua Budi menitipkan seduhan ke mesin kopi dan tetap melayani pelanggan. Kode yang membutuhkan waktu sebaiknya menitipkan pekerjaan, bukan memegang thread.

Kapan Sinkron Masih Tepat #

Blocking bukan hal yang wajib dihindari sepenuhnya. Perhitungan cepat — menghitung diskon, memformat tanggal, memvalidasi isian form — justru paling jelas ditulis secara sinkron: mudah dibaca, mudah diuji, dan selesai dalam mikrodetik sehingga tidak ada yang menunggu lama. Asynchronous dibutuhkan ketika ada waktu tunggu yang nyata, misalnya:

  • menunggu berkas dibaca dari disk;
  • menunggu data diambil dari server (section Network);
  • menunggu timer tertentu tercapai (chapter Timers);
  • menunggu pengguna menekan tombol (section Browser Events).

Aturan praktisnya: jika operasi selesai dalam hitungan mikrodetik, tulis sinkron; jika operasi mengukur waktunya dalam milidetik ke atas atau bergantung pada pihak lain, tulis asinkron.

Dengan dua perilaku dasar ini, satu pertanyaan besar tersisa: siapa yang memastikan callback yang dititipkan dipanggil pada giliran yang tepat? Jawabannya ada pada arsitektur inti runtime — event loop, yang kita buktikan langsung di chapter berikutnya.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

blocking-vs-nonblocking.js

Unduh berkas
javascript
// blocking-vs-nonblocking.js
// Simulasi blocking vs non-blocking di satu thread.

// --- 1. pekerjaan berat: loop sengaja menyibukkan thread ---
function hitungLaporan(durasiMs) {
  const mulai = Date.now();
  while (Date.now() - mulai < durasiMs) {
    // loop kosong: mensimulasikan perhitungan yang memakan waktu
  }
  return "laporan penjualan selesai dihitung";
}

// --- 2. skenario blocking: kasir menghitung laporan sendiri ---
console.log("A. Budi melayani pesanan pertama");
console.log("B. Budi mulai menghitung laporan...");
const hasilLaporan = hitungLaporan(30);
console.log("C.", hasilLaporan);
console.log("D. pesanan kedua dilayani setelah laporan selesai");

// --- 3. skenario non-blocking: seduhan didelegasikan ke mesin kopi ---
console.log("E. pesanan kopi Budi diterima");
setTimeout(() => {
  console.log("G. mesin kopi selesai, pesanan diantar");
}, 30);
console.log("F. Budi melayani pelanggan berikutnya");