Wasm vs JavaScript — WebAssembly

Wasm vs JavaScript: Kapan Pakai Yang Mana? WebAssembly dan JavaScript bukan musuh — mereka tim. Memahami kekuatan masing-masing membantu kamu memilih tool…

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;
  }
}

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)] };
}

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:

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

⚠️ Jebakan yang sering ketemu

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.

Yang akan kamu pelajari