Back to Blog
WebAssemblyPerformanceWeb DevelopmentTechnical

WebAssembly: A Practical Introduction for Web Developers

Understand WebAssembly and when to use it. From basic concepts to practical examples, learn how WASM can enhance your web applications.

B
Bootspring Team
Engineering
August 1, 2025
5 min read

WebAssembly (WASM) brings near-native performance to the web. While JavaScript remains the primary web language, WASM enables computation-heavy tasks that were previously impractical in browsers. Here's what you need to know.

What Is WebAssembly?#

WebAssembly is a binary instruction format that runs in browsers alongside JavaScript. It's:

  • Fast: Near-native execution speed
  • Safe: Runs in sandboxed environment
  • Portable: Same code runs everywhere
  • Language-agnostic: Compile from C, C++, Rust, Go, etc.

When to Use WebAssembly#

Good Use Cases#

✓ Computationally intensive tasks - Image/video processing - Cryptography - Physics simulations - Data compression ✓ Porting existing code - C/C++ libraries - Game engines - Desktop applications ✓ Performance-critical algorithms - Real-time audio processing - Machine learning inference - 3D rendering

When JavaScript Is Fine#

✗ DOM manipulation ✗ Simple data transformations ✗ UI interactions ✗ API calls ✗ Most business logic

Getting Started with Rust#

Setup#

1# Install Rust 2curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 3 4# Add WASM target 5rustup target add wasm32-unknown-unknown 6 7# Install wasm-pack 8cargo install wasm-pack

Simple Example#

1// src/lib.rs 2use wasm_bindgen::prelude::*; 3 4#[wasm_bindgen] 5pub fn fibonacci(n: u32) -> u32 { 6 match n { 7 0 => 0, 8 1 => 1, 9 _ => fibonacci(n - 1) + fibonacci(n - 2) 10 } 11} 12 13#[wasm_bindgen] 14pub fn process_image(pixels: &[u8]) -> Vec<u8> { 15 // Apply grayscale filter 16 pixels.chunks(4) 17 .flat_map(|rgba| { 18 let gray = (rgba[0] as f32 * 0.299 19 + rgba[1] as f32 * 0.587 20 + rgba[2] as f32 * 0.114) as u8; 21 vec![gray, gray, gray, rgba[3]] 22 }) 23 .collect() 24}

Build and Use#

# Build wasm-pack build --target web
1// Usage in JavaScript 2import init, { fibonacci, process_image } from './pkg/my_wasm.js'; 3 4async function main() { 5 await init(); 6 7 console.log(fibonacci(40)); // Faster than JS equivalent 8 9 const imageData = ctx.getImageData(0, 0, width, height); 10 const processed = process_image(new Uint8Array(imageData.data)); 11 imageData.data.set(processed); 12 ctx.putImageData(imageData, 0, 0); 13} 14 15main();

Memory Management#

Linear Memory#

1// WASM has a linear memory buffer 2const memory = new WebAssembly.Memory({ initial: 1 }); // 64KB pages 3 4// JavaScript can read/write to this memory 5const buffer = new Uint8Array(memory.buffer); 6buffer[0] = 42;

Passing Complex Data#

1// Rust side 2#[wasm_bindgen] 3pub fn sum_array(arr: &[f64]) -> f64 { 4 arr.iter().sum() 5} 6 7// For more complex types, use serde 8use serde::{Deserialize, Serialize}; 9 10#[derive(Serialize, Deserialize)] 11pub struct Point { 12 x: f64, 13 y: f64, 14} 15 16#[wasm_bindgen] 17pub fn distance(a: JsValue, b: JsValue) -> f64 { 18 let a: Point = serde_wasm_bindgen::from_value(a).unwrap(); 19 let b: Point = serde_wasm_bindgen::from_value(b).unwrap(); 20 ((b.x - a.x).powi(2) + (b.y - a.y).powi(2)).sqrt() 21}
// JavaScript side import { distance } from './pkg/my_wasm.js'; const d = distance({ x: 0, y: 0 }, { x: 3, y: 4 }); console.log(d); // 5

Async and Threading#

Web Workers#

1// worker.js 2importScripts('./pkg/my_wasm.js'); 3 4const { process_data } = wasm_bindgen; 5 6wasm_bindgen('./pkg/my_wasm_bg.wasm').then(() => { 7 self.onmessage = (e) => { 8 const result = process_data(e.data); 9 self.postMessage(result); 10 }; 11});

SharedArrayBuffer (Multi-threading)#

1// Requires enabling threads in wasm-pack 2use rayon::prelude::*; 3 4#[wasm_bindgen] 5pub fn parallel_sum(arr: &[f64]) -> f64 { 6 arr.par_iter().sum() 7}

Integration Patterns#

Streaming Compilation#

1// Load and compile in parallel with download 2const wasmModule = WebAssembly.compileStreaming( 3 fetch('./module.wasm') 4); 5 6const instance = await WebAssembly.instantiate( 7 await wasmModule, 8 imports 9);

Lazy Loading#

1// Load WASM only when needed 2let wasmModule = null; 3 4async function getWasmModule() { 5 if (!wasmModule) { 6 wasmModule = await import('./pkg/my_wasm.js'); 7 await wasmModule.default(); 8 } 9 return wasmModule; 10} 11 12// Usage 13button.addEventListener('click', async () => { 14 const wasm = await getWasmModule(); 15 const result = wasm.expensive_computation(data); 16});

Real-World Examples#

Image Processing#

1use image::{DynamicImage, GenericImageView}; 2 3#[wasm_bindgen] 4pub fn resize_image(data: &[u8], width: u32, height: u32) -> Vec<u8> { 5 let img = image::load_from_memory(data).unwrap(); 6 let resized = img.resize(width, height, image::imageops::FilterType::Lanczos3); 7 8 let mut output = Vec::new(); 9 resized.write_to(&mut output, image::ImageOutputFormat::Png).unwrap(); 10 output 11}

PDF Generation#

1use printpdf::*; 2 3#[wasm_bindgen] 4pub fn create_pdf(title: &str, content: &str) -> Vec<u8> { 5 let (doc, page, layer) = PdfDocument::new(title, Mm(210.0), Mm(297.0), "Layer"); 6 let font = doc.add_builtin_font(BuiltinFont::Helvetica).unwrap(); 7 8 let current_layer = doc.get_page(page).get_layer(layer); 9 current_layer.use_text(content, 12.0, Mm(10.0), Mm(280.0), &font); 10 11 doc.save_to_bytes().unwrap() 12}

Cryptography#

1use sha2::{Sha256, Digest}; 2 3#[wasm_bindgen] 4pub fn hash_password(password: &str, salt: &[u8]) -> Vec<u8> { 5 let mut hasher = Sha256::new(); 6 hasher.update(password.as_bytes()); 7 hasher.update(salt); 8 hasher.finalize().to_vec() 9}

Performance Comparison#

1// Benchmark: Fibonacci(40) 2console.time('JavaScript'); 3fibonacciJS(40); 4console.timeEnd('JavaScript'); // ~1500ms 5 6console.time('WebAssembly'); 7fibonacciWASM(40); 8console.timeEnd('WebAssembly'); // ~150ms (10x faster)

Debugging#

Browser DevTools#

// Enable WASM debugging in Chrome // chrome://flags/#enable-webassembly-debugging // Source maps (with wasm-pack) wasm-pack build --dev

Console Logging from Rust#

1use web_sys::console; 2 3#[wasm_bindgen] 4pub fn my_function() { 5 console::log_1(&"Hello from WASM!".into()); 6}

Best Practices#

Size Optimization#

# Optimize for size wasm-pack build --release # Further optimization with wasm-opt wasm-opt -O3 -o optimized.wasm input.wasm

Error Handling#

1use wasm_bindgen::prelude::*; 2 3#[wasm_bindgen] 4pub fn parse_json(input: &str) -> Result<JsValue, JsValue> { 5 serde_json::from_str(input) 6 .map(|v: serde_json::Value| serde_wasm_bindgen::to_value(&v).unwrap()) 7 .map_err(|e| JsValue::from_str(&e.to_string())) 8}

Conclusion#

WebAssembly opens new possibilities for web applications—bringing performance-critical operations to the browser that were previously only possible with native code.

Start with a clear use case where JavaScript performance is insufficient. Use Rust with wasm-bindgen for the best developer experience. Measure performance to ensure WASM actually helps your specific workload.

WebAssembly isn't a JavaScript replacement—it's a powerful complement for when you need that extra performance.

Share this article

Help spread the word about Bootspring