How to make this wasm + rust code faster than just pure js

I built this particle animation in rust + wasm and javascript and test it for 50000 x 8 times particle the javascript version is faster then rust + wasm is there a way to make it fast faster then js

 use wasm_bindgen::prelude::*;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, MouseEvent};
use js_sys::Math;
use std::f64;
use wasm_bindgen::JsCast;

fn random_int(min: i32, max: i32) -> f64 {
    Math::random() * (max - min) as f64 + min as f64
}

#[wasm_bindgen]
pub struct Particle {
    x: f64,
    y: f64,
    size: f64,
    sx: f64,
    sy: f64,
    color: String,
}

#[wasm_bindgen]
impl Particle {
    pub fn new(x: f64, y: f64) -> Particle {
        Particle {
            x,
            y,
            size: random_int(10, 15),
            sx: random_int(-3, 3),
            sy: random_int(-3, 3),
            color: format!("hsl({}, 100%, 50%)", random_int(0, 360)),
        }
    }

    pub fn draw(&self, ctx: &CanvasRenderingContext2d) {
        ctx.begin_path();
        ctx.set_fill_style(&JsValue::from_str(&self.color));
        ctx.arc(self.x, self.y, self.size, 0.0, 2.0 * f64::consts::PI).unwrap();
        ctx.fill();
        ctx.close_path();
    }

    pub fn update(&mut self, ctx: &CanvasRenderingContext2d) {
        self.draw(ctx);
        self.x += self.sx;
        self.y += self.sy;
        if self.size > 0.2 {
            self.size -= 0.08;
        }
    }
}

#[derive(Clone, Copy)]
struct Mouse {
    x: f64,
    y: f64,
}

#[wasm_bindgen]
pub fn start() -> Result<(), JsValue> {
    let window = web_sys::window().unwrap();
    let document = window.document().unwrap();
    
    let canvas: HtmlCanvasElement = document
        .get_element_by_id("myCanvas")
        .unwrap()
        .dyn_into::<HtmlCanvasElement>()?;
        
    let ctx: CanvasRenderingContext2d = canvas
        .get_context("2d")?
        .unwrap()
        .dyn_into::<CanvasRenderingContext2d>()?;
        
    let particles = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
    let mouse = std::rc::Rc::new(std::cell::RefCell::new(Mouse { x: 0.0, y: 0.0 }));
    
    let particles_clone = particles.clone();
    let mouse_clone = mouse.clone();
    
    let click_closure = Closure::wrap(Box::new(move |event: MouseEvent| {
        let mut mouse = mouse_clone.borrow_mut();
        mouse.x = event.client_x() as f64;
        mouse.y = event.client_y() as f64;
        let mut particles = particles_clone.borrow_mut();
        // Changed to 500 particles per click
        for _ in 0..50 {
            particles.push(Particle::new(mouse.x, mouse.y));
        }
    }) as Box<dyn FnMut(MouseEvent)>);
    
    canvas.add_event_listener_with_callback("mousemove", click_closure.as_ref().unchecked_ref())?;
    // canvas.add_event_listener_with_callback("click", click_closure.as_ref().unchecked_ref())?;
    click_closure.forget();

    let f = std::rc::Rc::new(std::cell::RefCell::new(None::<Closure<dyn FnMut()>>));
    let g = f.clone();

    let particles_clone = particles.clone();
    *g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
        let canvas = ctx.canvas().unwrap();
        ctx.clear_rect(0.0, 0.0, canvas.width() as f64, canvas.height() as f64);
        
        let mut particles = particles_clone.borrow_mut();
        // Remove particles that are too small
        particles.retain(|particle| particle.size > 0.2);
        // Update remaining particles
        for particle in particles.iter_mut() {
            particle.update(&ctx);
        }
        
        web_sys::window()
            .unwrap()
            .request_animation_frame(f.borrow().as_ref().unwrap().as_ref().unchecked_ref())
            .unwrap();
    }) as Box<dyn FnMut()>));

    web_sys::window()
        .unwrap()
        .request_animation_frame(g.borrow().as_ref().unwrap().as_ref().unchecked_ref())
        .unwrap();

    Ok(())
}

This sort of code is something of a “worst case” for Rust-on-web-pages because

  • You are sending very many individual calls (ctx.begin_path(), etc.) from Rust to JS, each one of which has to pass through the wasm-bindgen Rust/Wasm/JS bridge code.
  • There isn’t much computation happening on the Rust side, so there isn’t much benefit from the fact that Rust can do some computations more efficiently than JS.

One easy way to make it more efficient is by not constructing new JsValues every time you draw every particle — instead of storing the particle color as a String, store it as the JsValue (or JsString). That will avoid copying the string every time each particle is drawn.

A further improvement would be to avoid creating strings for each particle, by creating an array or vector of enough distinct colors and then cloneing a JsValue/JsString from that array instead of creating a new one.

But if you want to get really good particle throughput, you will need to transfer the particle data from Rust to JS in bulk, using Float32Array or similar. You could either write JavaScript code that reads the array and runs the canvas, or, likely even better, switch to WebGL/WebGPU so that there isn't even any JavaScript code running per particle, only GPU shader code. This can be done in Rust (and shader languages) without writing any JS.

1 Like

I recently started learning wasm. would it be better if to just do computation on rust part and reendering and calling mouse move eventlistner code in javascript. can you suggest some resourse

That would improve things quite a lot, yes.

Have you done this tutorial (game of life)?

https://rustwasm.github.io/docs/book/game-of-life/introduction.html

The way the pixels are calculated, bridged to JS, and rendered are good examples of efficient rust wasm imo.

I found this helpful: Making really tiny WebAssembly graphics demos - Cliffle. If I remember correctly, this Rust program writes directly to Canvas memory and has no dependencies.

1 Like

I have not looked hard at your code but when using Rust/WASM in the browser there is a huge overhead in moving data between the WASM code and the Javascript. So if your WASM computation is small it takes longer to move data between WASM and JS than actually doing the computation in JS.

I imagine that when rending a particle system a good approach would be to do all the calculation in WASM, and do all the rendering of it into a memory buffer in WASM. On completing updating that frame buffer send it over to JS for blitting into a canvas.

tsoding on YouTube has some videos showing how he does that in a simple game. He does a doom style 3D rending into a memory buffer, complete with multiple players, and lots of particle like bits from explosions and such. It renders at 60 frames per second: My Retro 3D Engine is Ready. You probably want to find the first video in that series.