Buka kekuatan pemrosesan paralel di JavaScript. Pelajari cara mengelola Promise konkuren dengan Promise.all, allSettled, race, dan any untuk aplikasi yang lebih cepat dan tangguh.
Menguasai Konkurensi JavaScript: Seluk Beluk Pemrosesan Promise Paralel
Dalam lanskap pengembangan web modern, performa bukanlah sebuah fitur; melainkan persyaratan mendasar. Pengguna di seluruh dunia mengharapkan aplikasi yang cepat, responsif, dan lancar. Inti dari tantangan performa ini, terutama di JavaScript, terletak pada konsep menangani operasi asinkron secara efisien. Mulai dari mengambil data dari API, membaca file, hingga melakukan kueri ke basis data, banyak tugas tidak selesai secara instan. Cara kita mengelola periode tunggu ini dapat menjadi pembeda antara aplikasi yang lamban dan pengalaman pengguna yang sangat lancar.
JavaScript, pada dasarnya, adalah bahasa utas tunggal (single-threaded). Ini berarti ia hanya dapat mengeksekusi satu bagian kode pada satu waktu. Ini mungkin terdengar seperti batasan, tetapi model event loop dan I/O non-blocking JavaScript memungkinkannya menangani tugas-tugas asinkron dengan efisiensi yang luar biasa. Landasan modern dari model ini adalah Promiseāsebuah objek yang merepresentasikan penyelesaian (atau kegagalan) dari sebuah operasi asinkron di masa mendatang.
Namun, hanya dengan menggunakan Promise atau sintaks `async/await` yang elegan tidak secara otomatis menjamin performa yang optimal. Kesalahan umum bagi para pengembang adalah menangani beberapa tugas asinkron yang independen secara berurutan, sehingga menciptakan penyumbatan (bottleneck) yang tidak perlu. Di sinilah pemrosesan promise secara konkuren berperan. Dengan meluncurkan beberapa operasi asinkron secara paralel dan menunggunya secara kolektif, kita dapat secara dramatis mengurangi total waktu eksekusi dan membangun aplikasi yang jauh lebih efisien.
Panduan komprehensif ini akan membawa Anda menyelami dunia konkurensi JavaScript. Kita akan menjelajahi alat-alat yang sudah ada di dalam bahasa iniā`Promise.all()`, `Promise.allSettled()`, `Promise.race()`, dan `Promise.any()`āuntuk membantu Anda mengatur tugas-tugas paralel seperti seorang profesional. Baik Anda seorang pengembang junior yang baru belajar tentang asinkronisitas maupun seorang insinyur berpengalaman yang ingin menyempurnakan pola kerja Anda, artikel ini akan membekali Anda dengan pengetahuan untuk menulis kode JavaScript yang lebih cepat, lebih tangguh, dan lebih canggih.
Pertama, Klarifikasi Singkat: Konkurensi vs. Paralelisme
Sebelum kita melanjutkan, penting untuk mengklarifikasi dua istilah yang sering digunakan secara bergantian tetapi memiliki makna yang berbeda dalam ilmu komputer: konkurensi dan paralelisme.
- Konkurensi adalah konsep mengelola beberapa tugas dalam periode waktu tertentu. Ini tentang menangani banyak hal sekaligus. Sebuah sistem bersifat konkuren jika dapat memulai, menjalankan, dan menyelesaikan lebih dari satu tugas tanpa menunggu tugas sebelumnya selesai. Dalam lingkungan single-threaded JavaScript, konkurensi dicapai melalui event loop, yang memungkinkan mesin beralih antar tugas. Saat satu tugas yang berjalan lama (seperti permintaan jaringan) sedang menunggu, mesin dapat mengerjakan hal lain.
- Paralelisme adalah konsep mengeksekusi beberapa tugas secara bersamaan. Ini tentang melakukan banyak hal sekaligus. Paralelisme sejati memerlukan prosesor multi-inti, di mana utas-utas yang berbeda dapat berjalan pada inti yang berbeda pada waktu yang bersamaan. Meskipun web worker memungkinkan paralelisme sejati di JavaScript berbasis browser, model konkurensi inti yang kita bahas di sini berkaitan dengan satu utas utama.
Untuk operasi yang terikat I/O (I/O-bound) seperti permintaan jaringan, model konkuren JavaScript memberikan *efek* paralelisme. Kita dapat memulai beberapa permintaan sekaligus. Sementara mesin JavaScript menunggu respons, ia bebas untuk melakukan pekerjaan lain. Operasi-operasi tersebut terjadi 'secara paralel' dari perspektif sumber daya eksternal (server, sistem file). Inilah model kuat yang akan kita manfaatkan.
Perangkap Sekuensial: Anti-Pola yang Umum
Mari kita mulai dengan mengidentifikasi kesalahan umum. Ketika pengembang pertama kali mempelajari `async/await`, sintaksnya begitu bersih sehingga mudah untuk menulis kode yang terlihat sinkron tetapi secara tidak sengaja menjadi sekuensial dan tidak efisien. Bayangkan Anda perlu mengambil profil pengguna, postingan terbaru mereka, dan notifikasi mereka untuk membangun sebuah dasbor.
Pendekatan yang naif mungkin terlihat seperti ini:
Contoh: Pengambilan Sekuensial yang Tidak Efisien
async function fetchDashboardDataSequentially(userId) {
console.time('sequentialFetch');
console.log('Fetching user profile...');
const userProfile = await fetchUserProfile(userId); // Menunggu di sini
console.log('Fetching user posts...');
const userPosts = await fetchUserPosts(userId); // Menunggu di sini
console.log('Fetching user notifications...');
const userNotifications = await fetchUserNotifications(userId); // Menunggu di sini
console.timeEnd('sequentialFetch');
return { userProfile, userPosts, userNotifications };
}
// Bayangkan fungsi-fungsi ini membutuhkan waktu untuk selesai
// fetchUserProfile -> 500ms
// fetchUserPosts -> 800ms
// fetchUserNotifications -> 1000ms
Apa yang salah dengan gambaran ini? Setiap kata kunci `await` menghentikan sementara eksekusi fungsi `fetchDashboardDataSequentially` hingga promise terselesaikan. Permintaan untuk `userPosts` bahkan tidak dimulai sampai permintaan `userProfile` selesai sepenuhnya. Permintaan untuk `userNotifications` tidak dimulai sampai `userPosts` kembali. Ketiga permintaan jaringan ini independen satu sama lain; tidak ada alasan untuk menunggu! Total waktu yang dibutuhkan akan menjadi jumlah dari semua waktu individu:
Total Waktu ā 500ms + 800ms + 1000ms = 2300ms
Ini adalah penyumbatan performa yang sangat besar. Kita bisa melakukan jauh lebih baik.
Membuka Performa: Kekuatan Eksekusi Konkuren
Solusinya adalah dengan memulai semua operasi asinkron sekaligus, tanpa langsung menunggunya dengan `await`. Ini memungkinkan mereka berjalan secara konkuren. Kita dapat menyimpan objek Promise yang tertunda dalam variabel dan kemudian menggunakan kombinator Promise (Promise combinator) untuk menunggu semuanya selesai.
Contoh: Pengambilan Konkuren yang Efisien
async function fetchDashboardDataConcurrently(userId) {
console.time('concurrentFetch');
console.log('Initiating all fetches at once...');
const profilePromise = fetchUserProfile(userId);
const postsPromise = fetchUserPosts(userId);
const notificationsPromise = fetchUserNotifications(userId);
// Sekarang kita menunggu semuanya selesai
const [userProfile, userPosts, userNotifications] = await Promise.all([
profilePromise,
postsPromise,
notificationsPromise
]);
console.timeEnd('concurrentFetch');
return { userProfile, userPosts, userNotifications };
}
Dalam versi ini, kita memanggil ketiga fungsi fetch tanpa `await`. Ini segera memulai ketiga permintaan jaringan. Mesin JavaScript menyerahkannya ke lingkungan yang mendasarinya (browser atau Node.js) dan menerima kembali tiga Promise yang tertunda. Kemudian, `Promise.all()` digunakan untuk menunggu ketiga promise ini terselesaikan. Total waktu yang dibutuhkan sekarang ditentukan oleh operasi yang berjalan paling lama, bukan jumlahnya.
Total Waktu ā max(500ms, 800ms, 1000ms) = 1000ms
Kita baru saja memotong waktu pengambilan data lebih dari setengahnya! Ini adalah prinsip dasar dari pemrosesan promise paralel. Sekarang, mari kita jelajahi alat-alat canggih yang disediakan JavaScript untuk mengatur tugas-tugas konkuren ini.
Perangkat Kombinator Promise: `all`, `allSettled`, `race`, dan `any`
JavaScript menyediakan empat metode statis pada objek `Promise`, yang dikenal sebagai kombinator promise. Masing-masing menerima sebuah iterable (seperti array) dari promise dan mengembalikan sebuah promise tunggal yang baru. Perilaku dari promise baru ini tergantung pada kombinator mana yang Anda gunakan.
1. `Promise.all()`: Pendekatan "Semua atau Tidak Sama Sekali"
`Promise.all()` adalah alat yang sempurna ketika Anda memiliki sekelompok tugas yang semuanya penting untuk langkah berikutnya. Ini merepresentasikan kondisi "DAN" logis: Tugas 1 DAN Tugas 2 DAN Tugas 3 semuanya harus berhasil.
- Input: Sebuah iterable dari promise.
- Perilaku: Ia mengembalikan satu promise yang terpenuhi (fulfill) ketika semua promise input telah terpenuhi. Nilai yang terpenuhi adalah sebuah array berisi hasil dari promise input, dalam urutan yang sama.
- Mode Kegagalan: Ia akan langsung menolak (reject) segera setelah salah satu promise input ditolak. Alasan penolakan adalah alasan dari promise pertama yang ditolak. Ini sering disebut perilaku "fail-fast".
Kasus Penggunaan: Agregasi Data Kritis
Contoh dasbor kita adalah kasus penggunaan yang sempurna. Jika Anda tidak dapat memuat profil pengguna, menampilkan postingan dan notifikasi mereka mungkin tidak masuk akal. Seluruh komponen bergantung pada ketersediaan ketiga titik data tersebut.
// Helper untuk mensimulasikan panggilan API
const mockApiCall = (value, delay, shouldFail = false) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldFail) {
reject(new Error(`Panggilan API gagal untuk: ${value}`));
} else {
console.log(`Selesai: ${value}`);
resolve({ data: value });
}
}, delay);
});
};
async function loadCriticalData() {
console.log('Menggunakan Promise.all untuk data kritis...');
try {
const [profile, settings, permissions] = await Promise.all([
mockApiCall('userProfile', 400),
mockApiCall('userSettings', 700),
mockApiCall('userPermissions', 500)
]);
console.log('Semua data kritis berhasil dimuat!');
// Sekarang render UI dengan profil, pengaturan, dan izin
} catch (error) {
console.error('Gagal memuat data kritis:', error.message);
// Tampilkan pesan kesalahan kepada pengguna
}
}
// Apa yang terjadi jika salah satu gagal?
async function loadCriticalDataWithFailure() {
console.log('\nMenunjukkan kegagalan Promise.all...');
try {
const results = await Promise.all([
mockApiCall('userProfile', 400),
mockApiCall('userSettings', 700, true), // Yang ini akan gagal
mockApiCall('userPermissions', 500)
]);
} catch (error) {
console.error('Promise.all ditolak:', error.message);
// Catatan: Panggilan 'userProfile' dan 'userPermissions' mungkin telah selesai,
// tetapi hasilnya hilang karena seluruh operasi gagal.
}
}
loadCriticalData();
// Setelah jeda, panggil contoh kegagalan
setTimeout(loadCriticalDataWithFailure, 2000);
Kelemahan `Promise.all()`
Kelemahan utamanya adalah sifat fail-fast-nya. Jika Anda mengambil data untuk sepuluh widget yang berbeda dan independen di sebuah halaman, dan satu API gagal, `Promise.all()` akan menolak, dan Anda akan kehilangan hasil dari sembilan panggilan lain yang berhasil. Di sinilah kombinator kita berikutnya unggul.
2. `Promise.allSettled()`: Sang Pengumpul yang Tangguh
Diperkenalkan di ES2020, `Promise.allSettled()` adalah sebuah terobosan untuk ketahanan (resilience). Ini dirancang untuk saat Anda ingin mengetahui hasil dari setiap promise, baik itu berhasil maupun gagal. Ia tidak akan pernah menolak (reject).
- Input: Sebuah iterable dari promise.
- Perilaku: Ia mengembalikan satu promise yang selalu terpenuhi (fulfill). Ia terpenuhi setelah semua promise input telah selesai (settled) (baik terpenuhi maupun ditolak). Nilai yang terpenuhi adalah sebuah array objek, masing-masing menjelaskan hasil dari sebuah promise.
- Format Hasil: Setiap objek hasil memiliki properti `status`.
- Jika terpenuhi: `{ status: 'fulfilled', value: theResult }`
- Jika ditolak: `{ status: 'rejected', reason: theError }`
Kasus Penggunaan: Operasi Independen yang Tidak Kritis
Bayangkan sebuah halaman yang menampilkan beberapa komponen independen: widget cuaca, umpan berita, dan ticker saham. Jika API umpan berita gagal, Anda tetap ingin menampilkan informasi cuaca dan saham. `Promise.allSettled()` sangat cocok untuk ini.
async function loadDashboardWidgets() {
console.log('\nMenggunakan Promise.allSettled untuk widget independen...');
const results = await Promise.allSettled([
mockApiCall('Data Cuaca', 600),
mockApiCall('Umpan Berita', 1200, true), // API ini sedang tidak aktif
mockApiCall('Ticker Saham', 800)
]);
console.log('Semua promise telah selesai. Memproses hasil...');
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Widget ${index} berhasil dimuat dengan data:`, result.value.data);
// Render widget ini ke UI
} else {
console.error(`Widget ${index} gagal dimuat:`, result.reason.message);
// Tampilkan status kesalahan spesifik untuk widget ini
}
});
}
loadDashboardWidgets();
Dengan `Promise.allSettled()`, aplikasi Anda menjadi jauh lebih tangguh. Satu titik kegagalan tidak menyebabkan efek berantai yang merusak seluruh antarmuka pengguna. Anda dapat menangani setiap hasil dengan baik.
3. `Promise.race()`: Yang Pertama Mencapai Garis Finis
`Promise.race()` melakukan persis seperti namanya. Ia mengadu sekelompok promise satu sama lain dan menyatakan pemenang segera setelah yang pertama melewati garis finis, tidak peduli apakah itu sukses atau gagal.
- Input: Sebuah iterable dari promise.
- Perilaku: Ia mengembalikan satu promise yang selesai (settles) (terpenuhi atau ditolak) segera setelah promise input yang pertama selesai. Nilai pemenuhan atau alasan penolakan dari promise yang dikembalikan akan sama dengan promise "pemenang".
- Catatan Penting: Promise-promise lainnya tidak dibatalkan. Mereka akan terus berjalan di latar belakang, dan hasilnya akan diabaikan begitu saja oleh konteks `Promise.race()`.
Kasus Penggunaan: Menerapkan Batas Waktu (Timeout)
Kasus penggunaan yang paling umum dan praktis untuk `Promise.race()` adalah untuk memberlakukan batas waktu (timeout) pada operasi asinkron. Anda dapat "mengadu" operasi utama Anda dengan promise `setTimeout`. Jika operasi Anda memakan waktu terlalu lama, promise batas waktu akan selesai terlebih dahulu, dan Anda dapat menanganinya sebagai kesalahan.
function createTimeout(delay) {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new Error(`Operasi melewati batas waktu setelah ${delay}ms`));
}, delay);
});
}
async function fetchDataWithTimeout() {
console.log('\nMenggunakan Promise.race untuk batas waktu...');
try {
const result = await Promise.race([
mockApiCall('beberapa data penting', 2000), // Ini akan memakan waktu terlalu lama
createTimeout(1500) // Ini akan memenangkan perlombaan
]);
console.log('Data berhasil diambil:', result.data);
} catch (error) {
console.error(error.message);
}
}
fetchDataWithTimeout();
Kasus Penggunaan Lain: Endpoint Redundan
Anda juga bisa menggunakan `Promise.race()` untuk melakukan kueri ke beberapa server redundan untuk sumber daya yang sama dan mengambil respons dari server mana pun yang tercepat. Namun, ini berisiko karena jika server tercepat mengembalikan kesalahan (misalnya, kode status 500), `Promise.race()` akan langsung menolak, bahkan jika server yang sedikit lebih lambat akan mengembalikan respons yang berhasil. Ini membawa kita ke kombinator terakhir kita, yang lebih cocok untuk skenario ini.
4. `Promise.any()`: Yang Pertama Berhasil
Diperkenalkan di ES2021, `Promise.any()` seperti versi yang lebih optimis dari `Promise.race()`. Ia juga menunggu promise pertama selesai, tetapi secara spesifik mencari yang pertama untuk terpenuhi (fulfill).
- Input: Sebuah iterable dari promise.
- Perilaku: Ia mengembalikan satu promise yang terpenuhi segera setelah salah satu promise input terpenuhi. Nilai pemenuhan adalah nilai dari promise pertama yang terpenuhi.
- Mode Kegagalan: Ia hanya akan menolak jika semua promise input ditolak. Alasan penolakan adalah objek `AggregateError` khusus, yang berisi properti `errors`āsebuah array dari semua alasan penolakan individu.
Kasus Penggunaan: Mengambil dari Sumber Redundan
Ini adalah alat yang sempurna untuk mengambil sumber daya dari beberapa sumber, seperti server utama dan cadangan atau beberapa Jaringan Pengiriman Konten (CDN). Anda hanya peduli untuk mendapatkan satu respons yang berhasil secepat mungkin.
async function fetchResourceFromMirrors() {
console.log('\nMenggunakan Promise.any untuk menemukan sumber sukses tercepat...');
try {
const resource = await Promise.any([
mockApiCall('CDN Utama', 800, true), // Gagal dengan cepat
mockApiCall('Mirror Eropa', 1200), // Lebih lambat tetapi akan berhasil
mockApiCall('Mirror Asia', 1100) // Juga berhasil, tetapi lebih lambat dari yang Eropa
]);
console.log('Sumber daya berhasil diambil dari mirror:', resource.data);
} catch (error) {
if (error instanceof AggregateError) {
console.error('Semua mirror gagal menyediakan sumber daya.');
// Anda dapat memeriksa kesalahan individual:
error.errors.forEach(err => console.log('- ' + err.message));
}
}
}
fetchResourceFromMirrors();
Dalam contoh ini, `Promise.any()` akan mengabaikan kegagalan cepat dari CDN Utama dan menunggu European Mirror terpenuhi, pada titik mana ia akan terselesaikan dengan data tersebut dan secara efektif mengabaikan hasil dari Asian Mirror.
Memilih Alat yang Tepat: Panduan Singkat
Dengan empat opsi yang kuat, bagaimana Anda memutuskan mana yang akan digunakan? Berikut adalah kerangka pengambilan keputusan yang sederhana:
- Apakah saya memerlukan hasil dari SEMUA promise, dan apakah akan menjadi bencana jika SALAH SATU dari mereka gagal?
GunakanPromise.all(). Ini untuk skenario yang terikat erat, semua-atau-tidak-sama-sekali. - Apakah saya perlu mengetahui hasil dari SEMUA promise, terlepas dari apakah mereka berhasil atau gagal?
GunakanPromise.allSettled(). Ini untuk menangani beberapa tugas independen di mana Anda ingin memproses setiap hasil dan menjaga ketahanan aplikasi. - Apakah saya hanya peduli pada promise pertama yang selesai, baik itu sukses maupun gagal?
GunakanPromise.race(). Ini terutama untuk mengimplementasikan batas waktu atau kondisi perlombaan lain di mana hasil pertama (jenis apa pun) adalah satu-satunya yang penting. - Apakah saya hanya peduli pada promise pertama yang BERHASIL, dan saya bisa mengabaikan yang gagal?
GunakanPromise.any(). Ini untuk skenario yang melibatkan redundansi, seperti mencoba beberapa endpoint untuk sumber daya yang sama.
Pola Lanjutan dan Pertimbangan Dunia Nyata
Meskipun kombinator promise sangat kuat, pengembangan profesional seringkali memerlukan sedikit lebih banyak nuansa.
Pembatasan Konkurensi dan Throttling
Apa yang terjadi jika Anda memiliki array berisi 1.000 ID dan Anda ingin mengambil data untuk masing-masing ID? Jika Anda secara naif memasukkan semua 1.000 panggilan yang menghasilkan promise ke dalam `Promise.all()`, Anda akan langsung menembakkan 1.000 permintaan jaringan. Ini dapat memiliki beberapa konsekuensi negatif:
- Beban Server Berlebih: Anda bisa membebani server yang Anda tuju, yang menyebabkan kesalahan atau penurunan performa bagi semua pengguna.
- Pembatasan Laju (Rate Limiting): Sebagian besar API publik memiliki batasan laju. Anda kemungkinan besar akan mencapai batas Anda dan menerima kesalahan `429 Too Many Requests`.
- Sumber Daya Klien: Klien (browser atau server) mungkin kesulitan mengelola begitu banyak koneksi jaringan yang terbuka sekaligus.
Solusinya adalah dengan membatasi konkurensi dengan memproses promise dalam batch (kelompok). Meskipun Anda dapat menulis logika sendiri untuk ini, pustaka yang matang seperti `p-limit` atau `async-pool` menanganinya dengan baik. Berikut adalah contoh konseptual tentang bagaimana Anda bisa melakukannya secara manual:
async function processInBatches(items, batchSize, processingFn) {
let results = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
console.log(`Memproses batch mulai dari indeks ${i}...`);
const batchPromises = batch.map(processingFn);
const batchResults = await Promise.allSettled(batchPromises);
results = results.concat(batchResults);
}
return results;
}
// Contoh penggunaan:
const userIds = Array.from({ length: 20 }, (_, i) => i + 1);
// Kita akan memproses 20 pengguna dalam batch berisi 5
processInBatches(userIds, 5, id => mockApiCall(`user_${id}`, Math.random() * 1000))
.then(allResults => {
console.log('\nPemrosesan batch selesai.');
const successful = allResults.filter(r => r.status === 'fulfilled').length;
const failed = allResults.filter(r => r.status === 'rejected').length;
console.log(`Total Hasil: ${allResults.length}, Berhasil: ${successful}, Gagal: ${failed}`);
});
Catatan tentang Pembatalan
Tantangan lama dengan Promise bawaan adalah bahwa mereka tidak dapat dibatalkan. Sekali Anda membuat promise, ia akan berjalan hingga selesai. Meskipun `Promise.race` dapat membantu Anda mengabaikan hasil yang lambat, operasi yang mendasarinya terus mengonsumsi sumber daya. Untuk permintaan jaringan, solusi modernnya adalah API `AbortController`, yang memungkinkan Anda memberi sinyal ke permintaan `fetch` bahwa ia harus dibatalkan. Mengintegrasikan `AbortController` dengan kombinator promise dapat menyediakan cara yang tangguh untuk mengelola dan membersihkan tugas-tugas konkuren yang berjalan lama.
Kesimpulan: Dari Pemikiran Sekuensial ke Konkuren
Menguasai JavaScript asinkron adalah sebuah perjalanan. Dimulai dengan memahami event loop single-threaded, berlanjut ke penggunaan Promise dan `async/await` untuk kejelasan, dan berpuncak pada berpikir secara konkuren untuk memaksimalkan performa. Beralih dari pola pikir `await` yang sekuensial ke pendekatan yang mengutamakan paralel adalah salah satu perubahan paling berdampak yang dapat dilakukan pengembang untuk meningkatkan responsivitas aplikasi.
Dengan memanfaatkan kombinator promise bawaan, Anda diperlengkapi untuk menangani berbagai skenario dunia nyata dengan elegan dan presisi:
- Gunakan `Promise.all()` untuk dependensi data yang kritis dan bersifat semua-atau-tidak-sama-sekali.
- Andalkan `Promise.allSettled()` untuk membangun UI yang tangguh dengan komponen independen.
- Gunakan `Promise.race()` untuk memberlakukan batasan waktu dan mencegah penungguan tanpa batas.
- Pilih `Promise.any()` untuk menciptakan sistem yang cepat dan toleran terhadap kesalahan dengan sumber data yang redundan.
Lain kali Anda mendapati diri Anda menulis beberapa pernyataan `await` secara berurutan, berhentilah sejenak dan tanyakan: "Apakah operasi-operasi ini benar-benar saling bergantung?" Jika jawabannya tidak, Anda memiliki peluang utama untuk merefaktor kode Anda untuk konkurensi. Mulailah menginisiasi promise Anda secara bersamaan, pilih kombinator yang tepat untuk logika Anda, dan saksikan performa aplikasi Anda meroket.