Rust for JavaScript Developers: Memory Safety, Performance & WebAssembly
A JavaScript developer's guide to Rust. Learn ownership vs garbage collection, Rust performance benchmarks, Cargo ecosystem, WebAssembly with wasm-pack, and when to choose Rust over JS.
Rust for JavaScript Developers: Memory Safety, Performance & WebAssembly
JavaScript developers who learn Rust donβt switch β they level up. You gain a second tool that solves the problems JavaScript genuinely canβt: predictable performance, no garbage collection pauses, and memory safety without a runtime. And thanks to WebAssembly, you can use Rust from your JavaScript code today.
This guide is written for working JS developers. Every Rust concept maps to something you already know.
Why JavaScript Developers Should Learn Rust
You already know how to write software. Rust wonβt teach you that β itβll teach you how computers actually work, then give you control over it.
Hereβs what Rust gives you that JS canβt:
- No garbage collector β no GC pauses, no memory pressure, fully predictable latency
- Zero-cost abstractions β high-level code with no runtime overhead
- Memory safety without GC β the compiler guarantees no null pointers, no use-after-free, no data races
- Near-C performance β Rust benchmarks consistently outperform JavaScript by 5-50x for CPU-intensive tasks
- WebAssembly first-class target β compile Rust to
.wasmand call it from JS
Popular tools you use daily that are written in Rust: Biome (formatter), SWC (transpiler), Turbopack (bundler), Volta (version manager), Zed (editor).
The Mental Model Shift: Ownership vs Garbage Collection
The single biggest concept in Rust is ownership. Once you get it, everything else follows.
How JavaScript Manages Memory
JavaScript uses a garbage collector (GC). You create objects, and the GC periodically finds objects with no more references and frees them. You never think about it β until a GC pause spikes your latency.
// JS: you allocate, GC frees
function processData() {
const data = [1, 2, 3, 4, 5] // allocated on heap
return data.reduce((a, b) => a + b, 0)
// data goes out of scope β GC will free it eventually
}
How Rust Manages Memory
Rust uses ownership rules enforced at compile time. No runtime required.
- Every value has exactly one owner
- When the owner goes out of scope, the value is freed immediately
- Values can be borrowed (referenced) without transferring ownership
fn process_data() -> i32 {
let data = vec![1, 2, 3, 4, 5]; // allocated on heap, owned by `data`
let sum: i32 = data.iter().sum();
sum
// `data` goes out of scope here β freed immediately, deterministically
}
Moves vs Copies vs Borrows
This is where JS developers get confused first. In JavaScript, assignments copy primitives and share references for objects:
// JS
const a = [1, 2, 3]
const b = a // b references same array
b.push(4)
console.log(a) // [1, 2, 3, 4] β a is affected!
In Rust, assigning a heap value moves it:
// Rust
let a = vec![1, 2, 3];
let b = a; // a is MOVED to b β a no longer valid
// println!("{:?}", a); // β compile error: "value borrowed after move"
println!("{:?}", b); // β
[1, 2, 3]
To use a value in multiple places without transferring ownership, borrow it:
let a = vec![1, 2, 3];
let b = &a; // b is an immutable reference (borrow) to a
println!("{:?}", a); // β
a still owns the data
println!("{:?}", b); // β
b is just a view into a
Mutable Borrows (Exclusive Access)
Rust enforces one rule at compile time that eliminates entire classes of bugs: you can have many immutable borrows OR one mutable borrow β never both at once.
let mut data = vec![1, 2, 3];
let ref1 = &data; // immutable borrow
let ref2 = &data; // another immutable borrow β fine!
println!("{:?} {:?}", ref1, ref2);
// ref1 and ref2 go out of scope here
data.push(4); // mutable borrow β fine now that immutable borrows are done
println!("{:?}", data); // [1, 2, 3, 4]
This prevents data races in multithreaded code at compile time. No runtime locks needed.
Rust Performance vs JavaScript
Letβs look at real numbers. These are approximate comparisons for common tasks:
| Task | JavaScript | Rust | Speedup |
|---|---|---|---|
| Fibonacci(40) | ~1,800ms | ~120ms | ~15x |
| SHA-256 hashing 1GB | ~3,200ms | ~180ms | ~18x |
| JSON parsing 100MB | ~800ms | ~90ms | ~9x |
| Regex on 10MB text | ~250ms | ~18ms | ~14x |
| Matrix multiply 1000x1000 | ~2,100ms | ~95ms | ~22x |
Benchmarks are illustrative; actual results vary by hardware and implementation.
This doesnβt mean you should rewrite everything in Rust. JavaScriptβs performance is excellent for most web tasks. Use Rust when:
- CPU-bound computations (image processing, video encoding, data crunching)
- Low-latency requirements (game engines, real-time systems)
- High-throughput servers (web servers handling 100k+ req/sec)
- CLI tools that need instant startup
Rust Syntax for JavaScript Developers
Most of the concepts are familiar β the syntax just looks different.
Variables
// Immutable by default (like const in JS)
let name = "Vic";
let age = 30;
// Mutable (like let in JS)
let mut count = 0;
count += 1;
// Type annotations (optional when inferable)
let score: f64 = 9.5;
let items: Vec<String> = Vec::new();
Functions
// Basic function
fn add(a: i32, b: i32) -> i32 {
a + b // no semicolon = implicit return
}
// Arrow function equivalent (closures)
let multiply = |a: i32, b: i32| -> i32 { a * b };
// Higher-order functions work like JS
let numbers = vec![1, 2, 3, 4, 5];
let doubled: Vec<i32> = numbers.iter().map(|n| n * 2).collect();
let evens: Vec<&i32> = numbers.iter().filter(|n| *n % 2 == 0).collect();
let sum: i32 = numbers.iter().sum();
Structs (Objects/Classes)
// Define a struct (like a JS class or object shape)
struct User {
name: String,
age: u32,
email: String,
}
// Implement methods
impl User {
// Constructor (Rust convention: `new`)
fn new(name: &str, age: u32, email: &str) -> Self {
User {
name: name.to_string(),
age,
email: email.to_string(),
}
}
fn greeting(&self) -> String {
format!("Hi, I'm {} and I'm {} years old", self.name, self.age)
}
}
let user = User::new("Vic", 28, "[email protected]");
println!("{}", user.greeting());
Enums and Pattern Matching
This is where Rust gets better than JavaScript. Enums are algebraic data types β each variant can hold different data.
// Like TypeScript union types, but more powerful
enum ApiResult {
Success { data: String, status: u16 },
Error { message: String, code: u16 },
NotFound,
}
fn handle_result(result: ApiResult) {
match result {
ApiResult::Success { data, status } => {
println!("Got {} with status {}", data, status);
}
ApiResult::Error { message, code } => {
eprintln!("Error {}: {}", code, message);
}
ApiResult::NotFound => {
println!("Resource not found");
}
}
// Compiler forces you to handle ALL variants β no forgotten cases
}
Error Handling: Result Instead of Try/Catch
Rust doesnβt have exceptions. Functions that can fail return Result<T, E>.
use std::fs;
use std::num::ParseIntError;
// Function that can fail
fn read_number_from_file(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let content = fs::read_to_string(path)?; // ? propagates the error
let number = content.trim().parse::<i32>()?;
Ok(number)
}
// Calling it
match read_number_from_file("data.txt") {
Ok(n) => println!("Got number: {}", n),
Err(e) => eprintln!("Failed: {}", e),
}
The ? operator is like an early return Err(...) β it propagates errors up the call stack. Think of it as .unwrap() that returns instead of panicking.
The Cargo Ecosystem
Cargo is Rustβs package manager. Itβs like npm, but it also handles building, testing, and documentation.
# Create a new project
cargo new my-project
cd my-project
# Build
cargo build
# Run
cargo run
# Test
cargo test
# Add a dependency
cargo add serde # Add serde (JSON serialization)
cargo add tokio --features full # Add tokio async runtime
Cargo.toml (Like package.json)
[package]
name = "my-project"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
reqwest = { version = "0.11", features = ["json"] }
Popular Crates for Web Developers
| JavaScript | Rust Equivalent |
|---|---|
| Express/Fastify | Axum, Actix-web |
| Prisma/Drizzle | Diesel, SQLx |
| Lodash | itertools |
| axios/fetch | reqwest |
| zod | validator, garde |
| JSON.parse | serde_json |
| crypto | ring, sha2 |
Async Rust with Tokio
use tokio;
use reqwest;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let response = reqwest::get("https://api.github.com/repos/rust-lang/rust")
.await?
.json::<serde_json::Value>()
.await?;
println!("Stars: {}", response["stargazers_count"]);
Ok(())
}
Rust + WebAssembly with wasm-pack
This is where Rust becomes directly useful for JavaScript developers. You can compile Rust to WebAssembly and call it from your existing JavaScript code.
Setting Up
# Install wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
# Create a new wasm library
cargo new --lib wasm-image-processor
cd wasm-image-processor
# Cargo.toml
[package]
name = "wasm-image-processor"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
Writing the Rust Code
// src/lib.rs
use wasm_bindgen::prelude::*;
// Expose this function to JavaScript
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
#[wasm_bindgen]
pub fn process_pixel_data(pixels: &[u8], brightness: f32) -> Vec<u8> {
pixels.iter().map(|&p| {
let adjusted = (p as f32 * brightness) as u32;
adjusted.min(255) as u8
}).collect()
}
Building and Using from JavaScript
wasm-pack build --target web
// In your JavaScript/TypeScript project
import init, { fibonacci, process_pixel_data } from './wasm-image-processor/pkg'
async function main() {
await init() // Initialize the WASM module
// Now call Rust functions from JS
console.log(fibonacci(40)) // ~15x faster than JS equivalent
// Process image data
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
const processed = process_pixel_data(imageData.data, 1.5)
const newImageData = new ImageData(processed, canvas.width, canvas.height)
ctx.putImageData(newImageData, 0, 0)
}
wasm-bindgen: Passing Complex Types
use wasm_bindgen::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct ProcessResult {
pub pixels_processed: u32,
pub elapsed_ms: f64,
pub checksum: u32,
}
#[wasm_bindgen]
pub fn batch_process(data: &[u8]) -> JsValue {
let result = ProcessResult {
pixels_processed: data.len() as u32,
elapsed_ms: 12.5,
checksum: data.iter().map(|&b| b as u32).sum(),
};
// Serialize to JS object
serde_wasm_bindgen::to_value(&result).unwrap()
}
const result = batch_process(data)
console.log(result.pixels_processed, result.elapsed_ms)
Common JS β Rust Patterns
String Handling
// Rust has two string types:
// - &str: string slice (borrowed, like a view)
// - String: owned heap string (like JS string but mutable)
let s1: &str = "hello"; // string literal, always valid
let s2: String = String::from("world"); // heap allocated
let s3 = format!("{} {}", s1, s2); // like template literals
let s4 = s1.to_string(); // convert &str β String
// Common operations
let upper = s1.to_uppercase();
let contains = s2.contains("or");
let split: Vec<&str> = s3.split(' ').collect(); // like .split()
let trimmed = " hello ".trim();
Collections
use std::collections::HashMap;
// Vec (like Array)
let mut nums: Vec<i32> = vec![1, 2, 3];
nums.push(4);
nums.retain(|&n| n % 2 == 0); // like .filter() in place
let doubled: Vec<i32> = nums.iter().map(|&n| n * 2).collect();
// HashMap (like Map/Object)
let mut map: HashMap<String, i32> = HashMap::new();
map.insert("alice".to_string(), 42);
map.insert("bob".to_string(), 37);
if let Some(age) = map.get("alice") {
println!("Alice is {}", age);
}
// Iterate
for (name, age) in &map {
println!("{}: {}", name, age);
}
Async/Await
// Rust async looks very similar to JS
async fn fetch_user(id: u32) -> Result<User, reqwest::Error> {
let url = format!("https://api.example.com/users/{}", id);
let user = reqwest::get(&url)
.await?
.json::<User>()
.await?;
Ok(user)
}
// Parallel async (like Promise.all)
use futures::future::join_all;
async fn fetch_all_users(ids: Vec<u32>) -> Vec<Result<User, reqwest::Error>> {
let futures: Vec<_> = ids.into_iter()
.map(|id| fetch_user(id))
.collect();
join_all(futures).await
}
When to Choose Rust Over JavaScript
Choose Rust when:
- CPU-intensive work (image/video processing, cryptography, data parsing)
- Building CLI tools (Rust binaries start in <1ms; Node.js takes ~100ms)
- Writing a library that will be called from multiple languages via FFI or WASM
- Building a high-throughput web server (Axum handles ~1M req/sec on modest hardware)
- You need deterministic memory usage and no GC pauses
- Systems programming: OS kernels, embedded devices, game engines
Stick with JavaScript when:
- Standard CRUD web applications
- Rapid prototyping
- Your team knows JS and the bottleneck isnβt performance
- Working primarily with DOM/browser APIs
- The I/O wait time dominates (Rust wonβt help if youβre waiting on a database)
Use both (WASM bridge):
- Compute-heavy parts in Rust, UI in JavaScript
- Porting C/C++ algorithms to the web
- Image/audio/video processing in the browser
Learning Path for JavaScript Developers
-
The Rust Book β read chapters 1-10 first. Ownership, structs, enums, traits.
-
Rustlings β small exercises to practice syntax. Like exercism but specifically for Rust concepts.
-
Rust by Example β pattern reference. Great for βhow do I do X in Rust?β
-
wasm-bindgen guide β once you know basic Rust, build a WASM module for your JS project.
-
Zero to Production in Rust β the best book for building production web services with Rust (Axum-based patterns).
Your First Rust Project
Build a CLI tool that replaces a Node.js script you already have:
cargo new my-cli
// src/main.rs
use std::env;
use std::fs;
fn main() {
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
eprintln!("Usage: my-cli <filename>");
std::process::exit(1);
}
let content = fs::read_to_string(&args[1])
.expect("Could not read file");
let word_count = content.split_whitespace().count();
let line_count = content.lines().count();
let char_count = content.chars().count();
println!("Lines: {}", line_count);
println!("Words: {}", word_count);
println!("Chars: {}", char_count);
}
cargo build --release
./target/release/my-cli README.md
Summary
Rust is worth learning as a JavaScript developer because:
- Ownership model explains memory at the level the CPU works β once understood, itβs not a burden
- 5-50x performance for CPU-bound tasks, accessed from JS via WASM
- No runtime overhead β ship a 2MB binary instead of bundling Node.js
- Compiler as safety net β the borrow checker catches bugs before they reach production
- Cargo ecosystem β package management that feels familiar
You donβt need to rewrite your app in Rust. Start by identifying one CPU-intensive function β a parser, an encoder, a hash function β and implement it in Rust with wasm-pack. Ship it as a WASM module. See the speedup. The rest follows naturally.
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links β we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.