สร้าง WebAssembly Module แรกกันเถอะ
By Arnon Puitrakul - 21 เมษายน 2026
จากบทความที่แล้ว ที่มาแนะนำ WebAssembly ไปว่า มันเป็นอะไรที่ค่อนข้างน่าสนใจ และน่าเรียนรู้เอาไว้มาก ๆ วันนี้เราจะมาพาทุกคนไปสร้าง WebAssembly Module แรกกัน และเทียบกันไปเลยว่า Performance ในงานที่ CPU Bound มาก ๆ มันจะห่างกันขนาดไหน
งานที่เราจะเอามาเล่นกันวันนี้ เป็นโปรแกรมง่าย ๆ อย่างโปรแกรมสำหรับหาลำดับ Fibonacci เป็นงานที่ค่อนข้าง CPU Bound น่าจะพอทำให้เห็นภาพของ Performance ในการทำงานมากขึ้น
เตรียมอุปกรณ์ที่จำเป็น
หลัก ๆ เครื่องมือที่เราจำเป็นต้องใช้ ไม่มีอะไรมาก นอกจาก AOT Compiler ที่แปลง C Source Code ให้กลายเป็น WASM Module โดยปกติ เราจะใช้งานเครื่องมืออย่าง Emscripten เป็นเครื่องมือที่คิดว่า น่าจะง่ายที่สุดในการทำงานแล้ว หรือถ้าสุดจัดพอ อยากได้อะไรที่ เบาบาง ไม่มีอะไรเลย ลองใช้ตัวที่ Built-in มาใน LLVM ได้ อันนั้นมีแต่ Compiler ล้วน ๆ ไม่มี Runtime Environment ใด ๆ ทั้งสิ้น แต่เพื่อความสะดวกในการ Demo เราขอใช้ Emscripten ละกัน
brew install emscriptenในการติดตั้ง Emscripten ทำได้ง่ายมาก ถ้ารันอยู่บน macOS หรือ Linux เราสามารถใช้ Homebrew ติดตั้งตรง ๆ เข้าไปได้เลย แค่รันคำสั่งด้านบน ก็เรียบร้อย
> emcc --version
emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 5.0.6-git
Copyright (C) 2026 the Emscripten authors (see AUTHORS.txt)
This is free and open source software under the MIT license.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.เพื่อความมั่นใจว่า การติดตั้งเรียบร้อย ลองรัน Compiler ขึ้นมาดู เราจะเห็นได้ว่าผลคือ มันจะเจอ Executable ของ Emscripten ก็แปลว่าติดตั้งเรียบร้อยแล้ว
เตรียม WASM Module
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVE
int fib(int n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
}ต่อไป เราจะมาเตรียม WASM Module กัน ให้เราสร้าง C Source Code File ขึ้นมา เขียนแบบด้านบนเลย หลัก ๆ เราจะเห็น Function สำหรับการคำนวณลำดับ Fibonacci แบบง่าย ๆ โดยใช้ Recursive
แต่ส่วนที่อยากให้โฟกัสคือ emscripten Header และ Macro ที่อยู่ด้านบนของ fib Function อย่าง EMSCRIPTEN_KEEPALIVE หน้าที่ของมันคือการบอก Compiler ว่า อย่าลบ Function นี้ทิ้งตอนทำ Optimisation นะ เพราะเดี๋ยวเราจะฝั่ง Javascript จะเรียกใช้จากภายนอก (ปกติ Compiler จะลบทิ้ง เพราะมันมองไม่เห็นว่ามีใครเรียก ในขั้นตอนของการทำ Optimisation)
emcc fib.c -s EXPORTED_FUNCTIONS="['_fib']" -o fib.wasm --no-entryจากนั้นเราจะมา Compile C Source Code ให้กลายเป็น WASM Module กัน โดยใช้งานคำสั่งด้านบน เรามาลองแกะ Argument กันทีละตัว อย่างแรกคือ Exported Function เป็นการบอกว่า เราต้องการ Export Function ชื่อ fib ออกไปให้ Javascript เรียกใช้ แล้วก็ -o fib.wasm คือบอก Output File สุดท้าย --no-entry คือ บอกว่า File นี้ไม่มี main Function นะ แค่นั้นเลย
เตรียม Javascript ผู้ท้าชิง
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>WASM vs JS: Fibonacci</title>
</head>
<body>
<script>
function jsFib(n) {
if (n <= 1) return n;
return jsFib(n - 1) + jsFib(n - 2);
}
</script>
</body>
</html>ต่อไป เราจะมาเตรียม Code ทางฝั่งผู้ท้าชิงกัน ขอไม่เรียนอะไรใน Body โชว์เลยนะ แค่สร้าง Function ชื่อ jsFib เอาไว้ ใช้ Logic เหมือนกันเป๊ะ กับที่เราเขียนใน C ก่อนหน้านี้เป๊ะ ๆ
เชื่อมลานประลอง
async function runBenchmark() {
const n = 40;
// WASM Benchmark
const response = await fetch('fib.wasm');
const wasmModule = await WebAssembly.instantiateStreaming(response);
const wasmFib = wasmModule.instance.exports.fib;
console.log(`🥊 Start Benchmarking at ${n} 🥊\n\n`);
const startWasm = performance.now();
const resultWasm = wasmFib(n);
const timeWasm = performance.now() - startWasm;
console.log(`[WASM] Answer: ${resultWasm}`);
console.log(`[WASM] Time: ${timeWasm.toFixed(2)} ms`);
console.log("-----------------------------------");
// JS Benchmark
const startJs = performance.now();
const resultJs = jsFib(n);
const timeJs = performance.now() - startJs;
console.log(`[JS] Answer: ${resultJs}`);
console.log(`[JS] Time: ${timeJs.toFixed(2)} ms`);
console.log("-----------------------------------");
// Diff
const speedup = (timeJs / timeWasm).toFixed(2);
console.log(`🚀 WebAssembly faster than JavaScript around ${speedup} time`);
}
// Init Benchmark
runBenchmark();ด้านบน เป็น Code ส่วนที่เราจะเอาไปต่อท้าย jsFib Function เพื่อเป็น Function สำหรับการทำ Benchmark ภายในเราจะเห็นว่ามันมี 2 ชุด ชุดแรกคือ ส่วนที่รัน Benchmark ของฝั่ง WASM และอีกฝั่งเป็นของ JS ในแต่ละการทดสอบ เราจะวัดเวลาด้วย performance Module ที่ติดมากับ JS อยู่แล้ว
จาก Code ด้านบน ในการเอา WASM Module เข้ามา เราจะต้องใช้คำสั่ง fetch File มันเข้ามา จากนั้นเราจะอ่านมันด้วย Function WebAssembly.instantiateStreaming สุดท้าย เราค่อยเอา Function fib ออกมาใส่เข้าไปในตัวแปร wasmFib แล้วก็สามารถเรียกใช้ได้เหมือน Function ปกติแล้ว
สุดท้าย เราจะต้อง Serve ผ่าน npx serve เพราะถ้าโหลดหน้าเว็บขึ้นมาเฉย ๆ จะติด CORS งั้นง่ายสุด ก็ Serve มันขึ้นมาซะเลย
ผลการทดลอง
🥊 Start Benchmarking at 40 🥊
(index):28 [WASM] Answer: 102334155
(index):29 [WASM] Time: 499.70 ms
(index):30 -----------------------------------
(index):37 [JS] Answer: 102334155
(index):38 [JS] Time: 582.50 ms
(index):39 -----------------------------------
(index):43 🚀 WebAssembly faster than JavaScript around 1.17 timeนี่คือผลที่ได้ออกมาจาก Console เราจะเห็นได้เลยว่า WASM Module คำนวณได้เร็วกว่าอยู่ประมาณ 1.17 เท่าตัว เมื่อเทียบกับ JS
เราทดสอบเพิ่มเติมด้วยการเอา C Source Code เดิมมาแก้ เพื่อใส่ Benchmark Set เข้าไป ปรากฏว่า ที่ลำดับ 40 เท่ากัน C Source Code ที่ Compile ด้วย gcc ใช้เวลาในการรัน 362.305 ms หรือห่างจาก WASM ราว ๆ 1.38 เท่าตัว หรือเทียบกับ JS จะห่างกัน 1.61 เท่าตัว
อาจจะเริ่มสงสัยแล้วว่า ทำไม WASM ทำเวลาได้ห่างจาก Vanilla JS น้อยจัง ไหนบอกว่า WASM มันเร็วกว่า JS มาก ๆ ไง ที่เรายกตัวอย่างนี้ จริง ๆ แล้วเราอยากจะนำเสนอว่า จริง ๆ แล้ว ไม่ใช่ทุกเคสที่ WASM จะคุ้มเสมอไป
เหตุผลที่ผลออกมาลักษณะนี้ เพราะ V8 Engine มันฉลาดกว่าที่เราคิดมาก ๆ เช่น มันเห็นแล้วว่า Function Fib โดนเรียกตัวเองซ้ำ ๆ มันจะทำการ Optimise JS ตัวนี้ให้เป็น Machine Code เก็บไว้ นั่นแปลว่า รอบหลังที่มันโดนแปลงแล้ว มันจะเหมือนกับเราเรียก Instruction ที่ผ่านการ Compile แล้วเลยละ อาจจะเสียเวลาช่วงแรกนิดหน่อยเพื่อ Warm Up แต่หลังจากนั้นคือ รัว ได้เลย และอีกอย่าง โจทย์นี้ มันไม่มีการสร้าง Object ไม่ต้องวุ่นวายกับ GC ซึ่งเป็นจุดอ่อนที่ทำให้ JS รันได้ช้าลง ทำให้ JS รีดพลัง CPU ออกมาได้เต็มที่ ดูจากตอนรันได้ CPU Utilisation สามารถรันได้เต็ม 100% ตลอดเวลาจนรันเสร็จเลย
ส่วนที่ WASM ช้ากว่า Native เป็นเพราะยังไง การรัน WASM มี Overhead มากกว่าการรันแบบ Native ตรง ๆ อย่างแน่นอน แต่การที่ WASM ทำได้ใกล้ขนาดนี้ ก็ถือว่าเก่งมาก ๆ แล้ว และต้องอย่าลืมนะว่า WASM Target เป็น Architecture Agnostic เราย้าย Platform ไป เราไม่ต้อง Recompile อะไรทั้งสิ้นเลยนะ แต่มันก็แลกมากับ การไม่ Utilise Instruction Set เฉพาะของ CPU นั้น ๆ เข้าไป เช่นใน ARM มี SIMD อย่าง ARM NEON ถ้าเรา Compile Target เป็น WASM โดยไม่ได้ใส่ Flag Native ไป เราก็จะไม่ได้ใช้ NEON เลย ทำออกมาก็กลาง ๆ ไม่ได้ Optimise เข้ากับ Hardware ได้ลึก และอีกอย่างที่สำคัญมากคือ ณ วันที่เขียน WASM มาตรฐานยังใช้ Pointer แบบ 32-bit อยู่ ทำให้บาง Algorithm ที่จัดการ 64-bit Register อาจจะทำงานได้ไม่เท่า Native 64-bit โดยตรงแน่นอน
จากการทดสอบนี้ ไม่ได้เป็นการบอกว่า WASM ไม่ดีเลย แต่เราว่าเราได้เรียนรู้กัน 2 เรื่องคือ Javascript ทรงพลังมากด้วย Modern Engine อย่าง V8 สำหรับงานที่เป็น Pure Computational แบบง่าย ๆ และ WebAssembly ให้ Performance ที่คาดเดาได้จริง แม้ JS จะทำ Performance ได้ใกล้เคียง แต่ JS ยังอาศัยพึ่งพาการเดา Type ของ JIT Compiler หากเดาผิดต้องทำ De-Optimisation ความเร็วจะตกฮวบทันที ในขณะที่ WASM มัน Compile มารอแล้ว ความเร็วจึงนิ่ง เสถียร ไว้ใจได้มากกว่าในสเกลงานที่ซับซ้อน
เท่าที่เราลองทำงาน เปลี่ยนจากการบวกเลขธรรมดาเป็นการทำ Support Vector Machine (SVM) บน Data Point หลักพันจุด ความแตกต่างบอกเลยว่า โดดไป 4.5 เท่าตัวได้เลย
สรุป
การสร้าง WASM Module แรกเพื่อทดลองรัน น่าจะเป็น Hello World ที่สมบูรณ์แล้วละ มันทำให้เราเห็นด้วยตาเลยว่า การเอา C มา Compile เพื่อรันใน Web Browser มันทำได้จริง และ JIT Compiler ของ Javascript นั้นทรงพลังมากกว่าที่เราคิด เมื่อต้องจัดการกับ Logic ง่าย ๆ
แน่นอนว่า ในโลกความเป็นจริง ไม่น่าจะมีคน Reimplement แล้ว Compile เป็น WASM เพียงเพื่อบวกเลขง่าย ๆ หรอก เขาจะเอามาใช้กันเมื่อ Application เราต้องการรีด Performance ของ Hardware สูงมาก ๆ หรืออยากจะ Port Legacy Application เก่า ๆ มาชีเสิร์ฟบน Web Application



