Wasm gak selalu lebih cepat dari JavaScript. Kadang lebih cepat, kadang sama, kadang malah lebih lambat, tergantung pekerjaannya dan cara kamu memanggilnya. Bayangin Bu Sri mengirim cucian dari Homestay Kabut ke laundry di bawah. Mobil bak (Wasm) bisa membawa banyak dan melaju kencang, tapi butuh waktu untuk muat dan bongkar barang. Motor (JavaScript) sudah parkir di depan pintu dan langsung jalan. Untuk satu karung besar, mobil menang. Untuk seratus kali bolak-balik membawa satu handuk, motor yang menang, karena tiap kali mobil harus muat dan bongkar lagi.
🧪 Tebakan cepat: foto Kamar Teras 1200 × 800 piksel mau dijadikan grayscale. Kalau fungsi Wasm-nya dipanggil sekali per piksel (960.000 kali), hasilnya lebih cepat atau lebih lambat dari JavaScript biasa? Tebak dulu, jawabannya ada di bagian "Ongkos menyeberang".
Pekerjaannya: foto Kamar Teras jadi abu-abu
Foto 1200 × 800 punya 960.000 piksel. Tiap piksel disimpan sebagai 4 byte: merah, hijau, biru, dan alfa (RGBA), jadi totalnya 3.840.000 byte. Rumus abu-abunya pakai bilangan bulat supaya hasil JavaScript dan Wasm bisa dibandingkan byte demi byte:
// abu = (r × 77 + g × 150 + b × 29) >> 8, bobotnya berjumlah 256
function grayscaleJs(px) {
for (let i = 0; i < px.length; i += 4) {
const abu = (px[i] * 77 + px[i + 1] * 150 + px[i + 2] * 29) >> 8;
px[i] = abu;
px[i + 1] = abu;
px[i + 2] = abu;
}
}
pxadalahUint8ClampedArray, deretan byte foto yang sama dengan yang dikasih canvas lewatgetImageData.i += 4lompat satu piksel.px[i + 3](alfa) gak disentuh.>> 8sama dengan membagi 256 lalu membuang pecahannya.
Versi Wasm-nya (foto.wasm, 256 byte) berisi loop yang sama, plus dua fungsi lain: abuPiksel(r, g, b) untuk satu piksel, dan totalPemesanan(n), loop angka biasa sebagai pembanding. Byte foto disalin dulu ke memori Wasm, diproses di sana, lalu disalin keluar lagi.
Cara mengukur yang jujur
function ukur(fn, kali = 10) {
const waktu = [];
for (let k = 0; k < kali; k++) {
const mulai = performance.now();
fn();
waktu.push(performance.now() - mulai);
}
const sisanya = waktu.slice(1).sort((a, b) => a - b);
return { pertama: waktu[0], median: sisanya[Math.floor(sisanya.length / 2)] };
}
performance.now()memberi waktu dalam milidetik dengan pecahan.Date.now()terlalu kasar untuk pekerjaan 1 ms.- Putaran pertama dicatat terpisah. Di putaran itu engine masih meng-compile dan mengoptimasi kodenya (warm-up).
- Ambil median dari putaran sisanya, bukan satu angka. Laptop yang sibuk bikin tiap putaran beda.
- Cek hasilnya sama persis. Benchmark yang hasilnya beda sedang membandingkan dua pekerjaan yang beda.
Hasilnya
Diukur di MacBook Pro M1 Pro yang sedang sibuk menjalankan banyak proses lain, median 9 putaran setelah putaran pertama, dalam milidetik. Di perangkatmu angkanya pasti beda; Lab di halaman ini mengukur ulang di browser-mu.
| Pekerjaan | Node 22.22 (V8 12.4) | Chromium 147 | WebKit 26.4 |
|---|---|---|---|
| Grayscale, JavaScript | 1,4 | 1,8 | 1,8 |
| Grayscale, Wasm (hitungnya saja) | 0,7 | 0,7 | 0,8 |
| Salin foto masuk dan keluar memori Wasm | 0,5 | 0,6 | 0,6 |
| Grayscale, Wasm dipanggil per piksel | 12 | 8 | 8 |
| Loop 20 juta pemesanan, JavaScript | 125 | 28 | 27 |
| Loop 20 juta pemesanan, Wasm | 18 | 18 | 17 |
| Loop Wasm, putaran pertama | 44 | 45 | 18 |
Hasil grayscale JavaScript dan Wasm sama persis byte demi byte, begitu juga hasil loop-nya (1107147376). Firefox yang bisa dijalankan di laptop ini (build Playwright 146) cuma punya compiler Wasm tingkat dasar, jadi angkanya gak dipakai: Firefox biasa punya compiler optimasi juga.
Yang bisa dibaca dari tabel:
- Hitungannya, Wasm sekitar 2,5 kali lebih cepat (1,8 ms jadi 0,7 ms di Chromium). Tapi ongkos menyalin foto masuk dan keluar 0,6 ms, jadi totalnya 1,3 ms lawan 1,8 ms. Menang, tapi tipis.
- Kode JavaScript yang sama, versi mesin beda, hasil beda jauh. Loop yang sama butuh 125 ms di Node 22 dan 28 ms di Chromium 147. Di Wasm,
i32.addmemang bekerja di 32 bit, jadi angkanya stabil 17 sampai 18 ms di mana-mana. Di JavaScript, angka aslinya float64, dan seberapa pintar engine mengubahnya ke 32 bit beda antar versi. - Wasm juga punya pemanasan. Di V8, putaran pertama 44 ms, berikutnya 18 ms: fungsi itu jalan dulu dengan compiler cepat Liftoff, lalu diganti versi TurboFan yang lebih cepat.
Ongkos menyeberang
Tiap panggilan dari JavaScript ke Wasm (dan sebaliknya) melewati perbatasan: argumen diubah ke tipe Wasm, lalu hasilnya diubah balik. Satu kali penyeberangan cuma beberapa nanodetik, tapi kalau dikali 960.000:
| Cara memanggil | Chromium 147 |
|---|---|
grayscale(0, 3840000), sekali untuk seluruh foto |
0,7 ms + 0,6 ms salin |
abuPiksel(r, g, b), sekali per piksel |
8 ms |
Jadi jawaban tebakannya: lebih lambat, sekitar 4 kali lebih lambat dari JavaScript biasa (8 ms lawan 1,8 ms). Aturannya: kirim datanya sekaligus, panggil sekali, biarkan loop-nya jalan di dalam Wasm.
Ongkos mulai: unduh, compile, instantiate
Sebelum fungsi pertama jalan, browser harus mengunduh .wasm, meng-compile-nya, lalu membuat instance. Untuk foto.wasm yang 256 byte, compile ditambah instantiate cuma 0,06 sampai 0,3 ms. Untuk modul besar, ongkosnya pindah tempat: V8 menunda compile tiap fungsi sampai fungsi itu pertama kali dipanggil (lazy compilation). Modul uji 1,3 MB berisi 4.000 fungsi di Node 22: compile() selesai dalam 3,6 ms, panggilan pertama ke semua fungsinya 44 ms, panggilan kedua 1,5 ms.
Untuk file dari server, pakai instantiateStreaming. Dia meng-compile sambil file-nya masih diunduh:
WebAssembly.instantiateStreaming(fetch('/wasm/foto.wasm')).then(({ instance }) => {
const { grayscale } = instance.exports;
// siap dipakai
});
Syaratnya server mengirim header Content-Type: application/wasm. Kalau gak, Chrome 147 menolak: TypeError: Failed to execute 'compile' on 'WebAssembly': Incorrect response MIME type. Expected 'application/wasm'. Di server lokal tanpa jeda jaringan, bedanya dengan fetch, arrayBuffer(), lalu instantiate gak kelihatan (di Chromium 147, modul 1,3 MB: 12 ms lawan 9 ms, masih noise). Untungnya baru terasa di jaringan yang lambat.
Kapan pakai yang mana
- Wasm: hitungan berat di data besar yang bisa dikirim sekaligus (pengolahan gambar, audio dan video, kompresi, kriptografi, fisika game), kode lama di C, C++, atau Rust yang mau dibawa ke web, dan pekerjaan yang butuh waktu stabil di semua browser.
- JavaScript: DOM, event,
fetch, logika UI, dan fungsi kecil yang dipanggil sering. Keterbatasan utama Wasm: dia gak bisa mengakses DOM secara langsung. Semua urusan halaman harus lewat JavaScript sebagai jembatan. - Polanya: JavaScript membaca foto dari canvas, Wasm mengolah pikselnya dalam satu panggilan, JavaScript menggambar hasilnya lagi.
⚠️ Jebakan yang sering ketemu
- Mengukur sekali. Putaran pertama ikut membayar compile dan pemanasan. Ukur berkali-kali dan pakai median.
- Lupa ongkos menyalin. Hitungan Wasm 0,7 ms terdengar hebat, sampai ditambah 0,6 ms untuk menyalin foto masuk dan keluar.
- Memanggil Wasm per item. 960.000 panggilan kecil lebih lambat dari satu loop JavaScript.
- Menyimpulkan dari satu browser. Loop yang sama: 125 ms di Node 22, 28 ms di Chromium 147.
TL;DR: Wasm gak otomatis lebih cepat. Di foto Kamar Teras 1200 × 800, hitungan grayscale Wasm 0,7 ms lawan JavaScript 1,8 ms di Chromium 147, tapi menyalin foto masuk dan keluar menambah 0,6 ms. Memanggil Wasm sekali per piksel (960.000 kali) malah 8 ms, lebih lambat dari JavaScript. Loop angka 20 juta: Wasm stabil 17 sampai 18 ms, JavaScript 28 ms di Chromium 147 tapi 125 ms di Node 22. Wasm juga punya pemanasan (44 ms lalu 18 ms di V8) dan ongkos mulai: unduh, compile, instantiate; pakai instantiateStreaming dengan Content-Type: application/wasm. Ukur dengan performance.now(), banyak putaran, median, dan hasil yang dicek sama. Pakai Wasm untuk hitungan berat yang datanya dikirim sekaligus; JavaScript untuk DOM, event, dan fungsi kecil, karena Wasm gak bisa menyentuh DOM langsung.