Menunggu satu permintaan jaringan mudah, tetapi program nyata hampir selalu memuat banyak sumber data sekaligus, dan cara menatanya menentukan cepat atau lambatnya hasil terasa. Topik ini membandingkan fetch berurutan dengan fetch paralel lewat Promise.all, membedah mengapa membuka ratusan permintaan sekaligus justru bermasalah, lalu membangun batas konkurensi (concurrency limit) dengan worker pool sederhana. Semua contoh berjalan di Node.js dengan server lokal sebagai pengganti layanan eksternal, sehingga polanya dapat direproduksi ulang tanpa menyentuh jaringan sungguhan.
Biaya fetch berurutan#
Skrip pertama mengambil tiga sumber data satu per satu, yaitu statistik, profil, dan catatan, dengan menunggu masing-masing selesai di dalam perulangan. Server lokalnya sengaja memberi jeda berbeda, 40, 20, dan 60 milidetik, sehingga urutan keluarannya dapat diprediksi. Port server ditentukan sistem lewat angka 0, dan jalur permintaannya membawa nama serta jeda setiap sumber data.
// Fetch berurutan: permintaan baru dimulai setelah permintaan sebelumnya selesai.
import http from "node:http";
// Server lokal menggantikan layanan eksternal, port ditentukan sistem lewat 0.
const server = http.createServer((req, res) => {
const url = new URL(req.url, "http://127.0.0.1");
const ms = Number(url.searchParams.get("ms") ?? "0");
const nama = url.searchParams.get("nama") ?? "?";
setTimeout(() => {
res.end(JSON.stringify({ nama, ms }));
}, ms);
});
await new Promise((selesai) => server.listen(0, "127.0.0.1", selesai));
const dasar = `http://127.0.0.1:${server.address().port}`;
const sumber = [
{ nama: "statistik", ms: 40 },
{ nama: "profil", ms: 20 },
{ nama: "catatan", ms: 60 },
];
for (const s of sumber) {
const res = await fetch(`${dasar}/ambil?nama=${s.nama}&ms=${s.ms}`);
const data = await res.json();
console.log(`selesai: ${data.nama}`);
}
server.close();
server.closeAllConnections();selesai: statistik
selesai: profil
selesai: catatanPolanya mudah dibaca, tetapi waktunya mahal. Total menunggu versi berurutan mendekati jumlah jeda semua permintaan, sekitar 120 milidetik untuk jeda 40, 20, dan 60 milidetik, karena permintaan ketiga baru dikirim setelah dua permintaan pertama benar-benar selesai. Dengan jumlah permintaan yang makin banyak, biayanya ikut menumpuk, padahal ketiga permintaan itu sebenarnya tidak saling bergantung dan tidak perlu menunggu satu sama lain.
Paralel dengan Promise.all#
Versi paralelnya mengirim ketiga permintaan hampir bersamaan dengan membangun array promise terlebih dahulu, lalu menunggu semuanya lewat Promise.all. Setiap promise mencatat kedatangannya sendiri, sehingga urutan keluaran menunjukkan urutan selesai yang sesungguhnya, bukan urutan daftar awal.
// Fetch paralel: ketiga permintaan dikirim bersamaan dan berlomba selesai.
import http from "node:http";
const server = http.createServer((req, res) => {
const url = new URL(req.url, "http://127.0.0.1");
const ms = Number(url.searchParams.get("ms") ?? "0");
const nama = url.searchParams.get("nama") ?? "?";
setTimeout(() => {
res.end(JSON.stringify({ nama, ms }));
}, ms);
});
await new Promise((selesai) => server.listen(0, "127.0.0.1", selesai));
const dasar = `http://127.0.0.1:${server.address().port}`;
const sumber = [
{ nama: "statistik", ms: 40 },
{ nama: "profil", ms: 20 },
{ nama: "catatan", ms: 60 },
];
const janji = sumber.map((s) =>
fetch(`${dasar}/ambil?nama=${s.nama}&ms=${s.ms}`)
.then((res) => res.json())
.then((data) => console.log(`diterima: ${data.nama}`))
);
await Promise.all(janji);
console.log("semua permintaan selesai");
server.close();
server.closeAllConnections();diterima: profil
diterima: statistik
diterima: catatan
semua permintaan selesaiPermintaan profil yang hanya menunggu 20 milidetik datang lebih dulu, diikuti statistik, dan catatan yang paling lambat menutup daftar. Total menunggunya kini mendekati jeda permintaan terlama saja, sekitar 60 milidetik, bukan jumlah ketiganya. Pada skala sepuluh permintaan dengan jeda serupa, selisih antara menjumlahkan semua jeda dan mengambil yang terlama menjadi semakin lebar, dan itulah alasan Promise.all menjadi pola bawaan untuk pekerjaan jaringan yang saling tidak bergantung.
Jebakan membuka semuanya sekaligus#
Paralel penuh bukan berarti tanpa batas. Membuka ratusan permintaan dengan pola yang sama berarti menumpuk ratusan koneksi dan promise yang hidup bersamaan, sedangkan server sasaran hampir selalu punya batas kapasitas, batas laju (rate limit), atau keduanya. Akibat yang lazim muncul berupa koneksi ditolak, permintaan menggantung sampai batas waktu (timeout), atau klien dihentikan sementara oleh server karena permintaannya dianggap berlebihan.
// tidak-dijalankan
// ratusan permintaan dikirim bersamaan tanpa batas
const janji = daftarUrl.map((url) => fetch(url));
const hasil = await Promise.all(janji);Browser sendiri membatasi jumlah koneksi HTTP/1.1 per origin, umumnya sekitar enam, sehingga masalahnya sering tersamar di sana karena permintaan berlebih hanya mengantre dengan tenang. Node.js tidak memberi batas serupa secara otomatis pada fetch-nya, sehingga skrip yang menaruh seribu URL ke Promise.all benar-benar menekan seribu koneksi sekaligus. Batas yang jelas harus dipasang sendiri oleh penulisnya, dan itulah pekerjaan bagian berikutnya.
Batas konkurensi dengan worker pool#
Worker pool menetapkan jumlah pekerja tetap, misalnya dua, dan setiap pekerja mengambil tugas berikutnya dari daftar selama masih ada sisa. Kuncinya satu variabel indeks yang diklaim secara bergantian, yaitu indeksBerikut++, sehingga dua pekerja tidak pernah memproses tugas yang sama. Tugas pada contohnya disimulasikan dengan penundaan 10 sampai 60 milidetik agar urutannya tetap dapat dilacak.
// Worker pool sederhana: paling banyak dua tugas berjalan pada satu waktu.
function tugasSimulasi(ms, nama) {
return new Promise((selesai) => setTimeout(() => selesai(nama), ms));
}
async function jalankanDenganBatas(daftarMs, batas) {
let indeksBerikut = 0;
async function pekerja() {
while (indeksBerikut < daftarMs.length) {
const nomor = indeksBerikut++;
console.log(`mulai tugas-${nomor}`);
await tugasSimulasi(daftarMs[nomor], nomor);
console.log(`selesai tugas-${nomor}`);
}
}
const kumpulanPekerja = [];
for (let i = 0; i < batas; i++) {
kumpulanPekerja.push(pekerja());
}
await Promise.all(kumpulanPekerja);
console.log(`semua tugas selesai dengan ${batas} pekerja`);
}
const durasi = [30, 10, 50, 20, 40, 60];
await jalankanDenganBatas(durasi, 2);mulai tugas-0
mulai tugas-1
selesai tugas-1
mulai tugas-2
selesai tugas-0
mulai tugas-3
selesai tugas-3
mulai tugas-4
selesai tugas-2
mulai tugas-5
selesai tugas-4
selesai tugas-5
semua tugas selesai dengan 2 pekerjaKeluarannya memperlihatkan mekanisme itu bekerja. Dua tugas mulai bersamaan, lalu setiap kali satu tugas selesai, pekerja yang menanganinya langsung mengambil tugas berikutnya, sehingga pada setiap saat paling banyak dua tugas berjalan. Total waktunya jauh lebih pendek daripada menjalankan semuanya berurutan, karena jeda tugas-tugas pendek diisi oleh pekerja lain alih-alih ditunggu diam.
Pola yang sama berpindah langsung ke pekerjaan nyata. Ganti tugasSimulasi dengan pemanggilan fetch ke sumber yang dituju, lalu bawa daftar URL sebagai masukannya. Sisa strukturnya tetap sama, yaitu indeks yang diklaim bergantian, pekerja yang mengantre tugas berikutnya, dan Promise.all yang menunggu seluruh pekerja selesai. Perbedaannya hanya pada tugas yang kini sungguhan menunggu jaringan, sehingga jumlah pekerjanya berperan sebagai katup yang menjaga permintaan aktif tetap terbatas.
Angka yang lazim dijadikan titik mulai untuk pekerjaan jaringan berkisar antara 4 sampai 8 pekerja, tetapi angka itu bukan aturan baku. Server yang memberlakukan batas laju bisa menuntut angka lebih kecil, sedangkan server yang sanggup melayani banyak permintaan bersamaan bisa menoleransi angka lebih besar. Satu-satunya penentu yang benar adalah pengukuran pada layanan yang dituju. Penambahan pekerja juga tidak menolong pekerjaan berat di sisi CPU, karena JavaScript tetap mengeksekusi hitungannya satu per satu pada satu thread yang sama.
Ringkasan#
- Fetch berurutan menunggu tiap permintaan, sehingga total waktunya mendekati jumlah jeda semua permintaan.
Promise.allmengirim permintaan bersamaan, sehingga totalnya mendekati jeda permintaan yang terlama.- Membuka ratusan permintaan sekaligus menekan server dan berisiko gagal massal, karena itu batas konkurensi dipasang sendiri.
- Worker pool dengan indeks tugas menjaga jumlah pekerjaan aktif tetap terbatas, dengan titik mulai 4 sampai 8 pekerja untuk jaringan yang kemudian diukur ulang.
Referensi#
- Promise.all() — MDN Web Docs — perilaku gabungan promise, termasuk urutan hasil dan kegagalannya yang menular.
- HTTP — Node.js — dokumentasi server http bawaan yang dipakai sebagai layanan lokal pada contoh.