Skip to main content
← All Articles Β· Β· 13 min read

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.

rustjavascriptwebassemblywasmperformancesystems-programmingmemory-safety

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 .wasm and 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.

  1. Every value has exactly one owner
  2. When the owner goes out of scope, the value is freed immediately
  3. 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:

TaskJavaScriptRustSpeedup
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"] }
JavaScriptRust Equivalent
Express/FastifyAxum, Actix-web
Prisma/DrizzleDiesel, SQLx
Lodashitertools
axios/fetchreqwest
zodvalidator, garde
JSON.parseserde_json
cryptoring, 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

  1. The Rust Book β€” read chapters 1-10 first. Ownership, structs, enums, traits.

  2. Rustlings β€” small exercises to practice syntax. Like exercism but specifically for Rust concepts.

  3. Rust by Example β€” pattern reference. Great for β€œhow do I do X in Rust?”

  4. wasm-bindgen guide β€” once you know basic Rust, build a WASM module for your JS project.

  5. 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:

  1. Ownership model explains memory at the level the CPU works β€” once understood, it’s not a burden
  2. 5-50x performance for CPU-bound tasks, accessed from JS via WASM
  3. No runtime overhead β€” ship a 2MB binary instead of bundling Node.js
  4. Compiler as safety net β€” the borrow checker catches bugs before they reach production
  5. 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.

Free Download

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.