Selain Rust, Go dan C/C++ juga bisa di-compile ke WebAssembly. Fungsinya bisa sama persis, tapi bawaan yang ikut masuk ke file .wasm beda jauh. Bayangin Bu Sri memesan kue lapis untuk tamu Homestay Kabut dari tiga toko. Toko pertama mengirim kuenya bersama koki dan petugas kebersihan yang ikut menginap. Toko kedua mengirim kue yang sama dengan satu asisten kecil. Toko ketiga mengirim kuenya saja, plus secarik kertas cara penyajian. Kuenya adalah fungsi hargaInap dan grayscale. Koki dan petugas kebersihan itu runtime Go (scheduler goroutine dan garbage collector), asisten kecilnya runtime TinyGo, dan kertas cara penyajiannya glue code JavaScript dari Emscripten.
🧪 Tebakan cepat:
hargaInapdangrayscaleditulis dalam Go lalu di-build dengango build, dan ditulis dalam C lalu di-build dengan clang. File.wasmversi Go kira-kira berapa kali lebih besar: 5 kali, 100 kali, atau lebih dari 5.000 kali? Tebak dulu, jawabannya ada di bagian "Ukurannya, diukur".
Go: GOOS=js, GOARCH=wasm, dan wasm_exec.js
Compiler Go biasa bisa langsung menghasilkan Wasm. Fungsi Go dipasang ke JavaScript lewat package syscall/js:
// main.go
package main
import "syscall/js"
func hargaInap(malam, hargaPerMalam int32) int32 {
return malam * hargaPerMalam
}
func grayscale(px []byte) {
for i := 0; i+3 < len(px); i += 4 {
abu := byte((uint32(px[i])*77 + uint32(px[i+1])*150 + uint32(px[i+2])*29) >> 8)
px[i], px[i+1], px[i+2] = abu, abu, abu
}
}
func main() {
js.Global().Set("hargaInap", js.FuncOf(func(this js.Value, args []js.Value) any {
return hargaInap(int32(args[0].Int()), int32(args[1].Int()))
}))
js.Global().Set("grayscale", js.FuncOf(func(this js.Value, args []js.Value) any {
buf := make([]byte, args[0].Get("length").Int())
js.CopyBytesToGo(buf, args[0]) // salin Uint8Array ke slice Go
grayscale(buf)
js.CopyBytesToJS(args[0], buf) // salin hasilnya balik
return nil
}))
select {} // jangan biarkan main selesai
}
$ GOOS=js GOARCH=wasm go build -o harga.wasm .
$ cp "$(go env GOROOT)/lib/wasm/wasm_exec.js" .
GOOS=js GOARCH=wasmmeminta Go membuat Wasm untuk lingkungan JavaScript (browser atau Node). Pelajaran ini memakai Go 1.27.2.js.FuncOfmembungkus fungsi Go jadi fungsi JS,js.Global().Setmemasangnya diglobalThis. Argumen datang sebagaijs.Value, jadi angka dibaca dengan.Int().CopyBytesToGodanCopyBytesToJSmenyalin byte foto dariUint8Arrayke memori Go, lalu balik lagi.select {}membuatmainmenunggu selamanya. Kalaumainselesai, program Go berhenti dan fungsi yang dipasang ikut mati.wasm_exec.jsadalah glue code-nya: modul ini meminta 21 imporgojs(jam, timer, akses objek JS), danwasm_exec.jsyang menyediakannya. Sejak Go 1.24 file ini ada dilib/wasm, sebelumnya dimisc/wasm.
Di halaman, const go = new Go(), lalu WebAssembly.instantiateStreaming(fetch('harga.wasm'), go.importObject), lalu go.run(instance). Setelah itu hargaInap(2, 350000) mengembalikan 700000.
Hasilnya: harga.wasm 2.042.356 byte (593.144 byte setelah gzip). Hampir semuanya runtime Go: garbage collector, scheduler goroutine, dan dukungan syscall/js. Penyeberangannya juga mahal: tiap panggilan lewat js.FuncOf membangunkan scheduler Go. Di Node 22, 100.000 panggilan hargaInap makan sekitar 780 ms. 100.000 panggilan export Wasm biasa (dari TinyGo atau C di bawah) cuma sekitar 0,7 ms.
📌 Go tanpa syscall/js: GOOS=wasip1 dan //go:wasmexport
Go 1.21 menambah target GOOS=wasip1 (WASI Preview 1, antarmuka sistem untuk Wasm di luar browser). Go 1.24 menambah directive //go:wasmexport, yang meng-export fungsi Go langsung sebagai export Wasm, dan -buildmode=c-shared untuk modul library (reactor) yang gak menjalankan main:
//go:wasmexport hargaInap
func hargaInap(malam, hargaPerMalam int32) int32 {
return malam * hargaPerMalam
}
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o harga.wasm . menghasilkan 1.931.836 byte dengan 10 impor wasi_snapshot_preview1 (jam, angka acak, tulis ke stdout, dan lainnya). Di Node, impor itu disediakan node:wasi, dan wasi.initialize(instance) harus dipanggil sebelum hargaInap. Untuk grayscale, modulnya meng-export pesanBuffer(n) supaya JS menulis foto ke tempat yang dipesan Go sendiri. Ukurannya tetap sekitar 2 MB, karena runtime-nya sama.
TinyGo: Go yang sama, file jauh lebih kecil
TinyGo adalah compiler Go lain yang dibangun di atas LLVM. Kode yang sama di-build dengan TinyGo 0.43.0:
| Perintah | Ukuran .wasm |
|---|---|
tinygo build -target=wasm -o harga.wasm . (versi syscall/js) |
236.916 byte |
sama, ditambah -no-debug |
54.371 byte |
tinygo build -target=wasip1 -buildmode=c-shared -no-debug ... |
27.767 byte |
tinygo build -target=wasm-unknown -no-debug ... |
2.567 byte, 0 impor |
- Lebih kecil karena runtime TinyGo sederhana (garbage collector dan scheduler kecil), dan LLVM membuang semua kode yang gak pernah dipanggil.
- Harganya: gak semua package standar dan fitur
reflectdidukung, dan garbage collector-nya lebih lambat untuk program yang banyak alokasi. - Tanpa
-no-debug, info debug (DWARF) ikut tersimpan. Itu sebabnya 236.916 byte. -target=wasmbutuhwasm_exec.jsmilik TinyGo (17.729 byte), bukan milik Go.- Modul
wasm-unknownbisa di-instantiate tanpa glue, asal_initialize()dipanggil dulu. Lab di halaman ini menjalankan modul 2.567 byte itu.
C dengan Emscripten
Emscripten adalah toolchain C/C++ ke Wasm: compiler emcc (di atas clang/LLVM), libc, dan glue code JavaScript yang meniru lingkungan C (heap, file system virtual, OpenGL lewat WebGL). Pelajaran ini memakai Emscripten 6.0.12.
// harga.c
#include <emscripten.h>
#include <stdint.h>
EMSCRIPTEN_KEEPALIVE
int32_t hargaInap(int32_t malam, int32_t hargaPerMalam) {
return malam * hargaPerMalam;
}
EMSCRIPTEN_KEEPALIVE
void grayscale(uint8_t *px, int32_t len) {
for (int32_t i = 0; i + 3 < len; i += 4) {
uint8_t abu = (px[i] * 77 + px[i + 1] * 150 + px[i + 2] * 29) >> 8;
px[i] = abu;
px[i + 1] = abu;
px[i + 2] = abu;
}
}
$ emcc harga.c -O2 -sMODULARIZE -sEXPORT_NAME=createHarga \
-sEXPORTED_FUNCTIONS=_malloc,_free -sEXPORTED_RUNTIME_METHODS=ccall,cwrap,HEAPU8 \
-o harga.js
createHarga().then((M) => {
console.log(M._hargaInap(2, 350000)); // 700000
console.log(M.ccall('hargaInap', 'number', ['number', 'number'], [2, 350000])); // 700000
const ptr = M._malloc(4); // pesan 4 byte di heap C
M.HEAPU8.set([200, 120, 40, 255], ptr); // tulis satu piksel
M._grayscale(ptr, 4);
console.log(M.HEAPU8.slice(ptr, ptr + 4)); // Uint8Array(4) [135, 135, 135, 255]
M._free(ptr); // kembalikan ke heap
});
EMSCRIPTEN_KEEPALIVEmenjaga fungsi supaya gak dibuang optimizer dan ikut di-export. Cara lain tanpa mengubah kode C:-sEXPORTED_FUNCTIONS=_hargaInap,_grayscale. Nama C di situ diberi awalan garis bawah, begitu juga di JS (M._hargaInap).-sMODULARIZE -sEXPORT_NAME=createHargamembuatharga.jsberisicreateHarga(), yang mengembalikan Promise berisi modulnya._malloc,_free, danHEAPU8harus diminta, karena JS butuh ketiganya untuk menaruh foto di memori C.ccallmemanggil fungsi C lewat namanya.- Hasilnya dua file:
harga.wasm7.174 byte (malloc ikut masuk) danharga.js10.511 byte. Tanpa_mallocdan runtime methods,emcc harga.c -O2 -o harga.jscuma 426 byte ditambah 6.424 byte glue.
C tanpa glue: clang --target=wasm32
clang bisa menghasilkan Wasm tanpa Emscripten, asal kodenya gak butuh libc. Apple clang bawaan macOS gak punya target Wasm; pakai clang dari LLVM, wasi-sdk, atau yang ikut emsdk. harga-polos.c sama dengan harga.c, tapi tanpa #include <emscripten.h>, dan tiap EMSCRIPTEN_KEEPALIVE diganti __attribute__((export_name("hargaInap"))) (dan "grayscale"):
$ clang --target=wasm32 -nostdlib -O2 -Wl,--no-entry -Wl,--export=__heap_base \
-Wl,--strip-all harga-polos.c -o harga-polos.wasm
-nostdlib: tanpa libc.-Wl,--no-entry: gak adamain.memoryikut di-export otomatis.__heap_baseadalah alamat pertama setelah stack dan data C. Di build ini nilainya 65.536, tepat di ujung memori awal (1 halaman). Jadi JS harusmemory.growdulu sebelum menaruh foto di situ; tanpa itu,setgagal denganRangeError: offset is out of bounds.- Hasilnya 363 byte, tanpa impor. JS-nya kamu urus sendiri: grow,
new Uint8Array(memory.buffer).set(foto, ptr), lalugrayscale(ptr, foto.length).
Ukurannya, diukur
Semua build berisi hargaInap dan grayscale dengan rumus yang sama:
| Build | .wasm |
Glue JS |
|---|---|---|
Go 1.27.2, GOOS=js |
2.042.356 byte | wasm_exec.js 16.992 byte |
Go 1.27.2, GOOS=wasip1, c-shared |
1.931.836 byte | host WASI |
TinyGo 0.43.0, -target=wasm -no-debug |
54.371 byte | wasm_exec.js 17.729 byte |
| Rust + wasm-pack (pelajaran sebelumnya) | 14.896 byte | 5.311 byte |
Emscripten 6.0.12, dengan _malloc dan HEAPU8 |
7.174 byte | 10.511 byte |
TinyGo 0.43.0, -target=wasm-unknown -no-debug |
2.567 byte | gak ada |
clang 24, --target=wasm32 -nostdlib |
363 byte | gak ada |
Jawaban tebakannya: lebih dari 5.000 kali. 2.042.356 dibagi 363 kira-kira 5.600. Hampir seluruh selisihnya runtime Go, bukan fungsi kita, dan setelah gzip pun versi Go masih 593.144 byte.
Kapan pakai yang mana:
- Go standar: kode Go yang sudah ada, butuh package standar dan goroutine, dan file sekitar 2 MB bisa diterima.
- TinyGo: modul Go kecil untuk browser, edge, atau plugin, selama package-nya didukung TinyGo.
- Emscripten: membawa library C/C++ besar ke web (SQLite, ffmpeg, game engine). Dia menyediakan libc, file system virtual, dan OpenGL lewat WebGL.
- clang polos: fungsi C kecil tanpa libc, saat file harus sekecil mungkin dan kamu siap mengurus memorinya sendiri.
⚠️ Jebakan yang sering ketemu
- Lupa
select {}dimainGo.mainselesai, runtime berhenti, dan panggilan berikutnya gagal:Error: Go program has already exited. wasm_exec.jsbeda versi. Salin dari Go yang sama dengan yang meng-compile. Untuk TinyGo, pakaiwasm_exec.jsmilik TinyGo. Keduanya gak bisa ditukar.- Lupa
_initialize()di modul TinyGowasm-unknown. Runtime belum siap, danhargaInapberhenti denganRuntimeError: unreachable. - Lupa
EMSCRIPTEN_KEEPALIVEatau-sEXPORTED_FUNCTIONS. Dengan-O2fungsinya dibuang:TypeError: M._hargaInap is not a function. LupaHEAPU8diEXPORTED_RUNTIME_METHODS: build-O0berhenti denganAborted('HEAPU8' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the Emscripten FAQ)), build-O2cuma memberiundefined. - Overflow di C.
hargaInap(7000, 350000)keluar-1844967296seperti di WAT, tapi di C overflowint32_tadalah undefined behavior: compiler boleh menganggapnya gak pernah terjadi. Untuk angka besar pakaiint64_t.
TL;DR: Go standar (GOOS=js GOARCH=wasm dengan syscall/js, js.FuncOf, select {}, dan wasm_exec.js dari lib/wasm di Go yang sama) menghasilkan 2.042.356 byte untuk hargaInap dan grayscale, karena runtime dan garbage collector Go ikut; panggilan lewat js.FuncOf juga lambat. GOOS=wasip1 (Go 1.21) dengan //go:wasmexport dan -buildmode=c-shared (Go 1.24) tetap sekitar 1,9 MB. TinyGo memakai runtime kecil dan LLVM: 54.371 byte (-target=wasm -no-debug) sampai 2.567 byte (-target=wasm-unknown, panggil _initialize() dulu), dengan dukungan package yang lebih terbatas. Emscripten (emcc) adalah toolchain C/C++ lengkap: EMSCRIPTEN_KEEPALIVE atau -sEXPORTED_FUNCTIONS=_nama, glue .js, _malloc/HEAPU8/_free untuk data. clang --target=wasm32 -nostdlib tanpa glue: 363 byte, memorinya kamu urus sendiri.