A WebAssembly (röviden Wasm) mára stabil, minden modern böngészőben támogatott technológia, és a Rust ökoszisztéma kifejezetten jól felkészült arra, hogy ebbe a világba exportáljuk a kódunkat. Ma megnézzük, hogyan indíthatsz el egy Rust-projektet, ami a böngészőben fut, és hogyan kommunikál a JavaScripttel.

Miért érdemes Rust-ot használni frontend logikához

A JavaScript kiváló nyelv a DOM manipulálására és az események kezelésére, de amikor számításigényes feladatokról van szó – képfeldolgozás, fizikai szimuláció, kriptográfia, parsing – bizony megfájdulhat a fejünk a teljesítménytől. Itt jön képbe a WebAssembly: bináris formátum, amit a böngésző motorja majdnem natív sebességgel tud futtatni.

A Rust ehhez azért ideális, mert:

  • Nincs garbage collector, tehát nincs GC-szünet, ami megakaszthatja az animációt vagy a real-time feldolgozást.
  • Az ownership rendszer memóriabiztosságot ad futásidejű költség nélkül – ez különösen fontos, ha a Wasm modulodnak nagy adatstruktúrákkal kell dolgoznia.
  • A típusrendszer és a Result/Option kombináció sokkal megbízhatóbb hibakezelést biztosít, mint a JavaScript kivételalapú megközelítése.
Tipp

Nem kell az egész frontendet Rust-ra átírnod! A leggyakoribb és leghasznosabb minta az, hogy csak a számításigényes „hot path”-eket írod meg Rust-ban, a UI-t és az eseménykezelést pedig JavaScriptben (vagy TypeScriptben) hagyod.

wasm-pack telepítése és projekt inicializálása

A wasm-pack a legegyszerűbb módja annak, hogy Rust crate-ből böngészőbarát Wasm-csomagot építs. Telepítsd a következő paranccsal (feltéve, hogy már van Rust toolchain-ed, jelen esetben stabil 1.68-as):

cargo install wasm-pack

Ezután hozz létre egy új könyvtár típusú crate-et:

cargo new --lib hello-wasm
cd hello-wasm

A Cargo.toml-ban jelöld ki, hogy a crate típusa cdylib legyen, és add hozzá a wasm-bindgen függőséget:

[package]
name = "hello-wasm"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2"
Megjegyzés

A cdylib crate-típus azt jelenti, hogy a fordító egy dinamikus könyvtárat épít, amit a WebAssembly runtime tud betölteni. Az rlib hozzáadása azért hasznos, hogy a crate-et natív Rust tesztekben is tudd használni cargo test-tel.

wasm-bindgen attribútumok: JavaScript és Rust közötti kommunikáció

A wasm-bindgen a híd a Rust és a JavaScript világ között. A #[wasm_bindgen] attribútummal jelölt függvények, struct-ok és enum-ok automatikusan exportálódnak úgy, hogy JavaScriptből hívhatók legyenek.

Írjunk egy egyszerű üdvözlő függvényt:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Szia, {}! Ez itt Rust kód a böngészőben.", name)
}

Ha a JavaScript oldalon szeretnél Rust felől hívható konzol logolást, ahhoz importálnod kell a böngésző API-ját. Ehhez az extern blokk és a #[wasm_bindgen] kombinációja szükséges:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);
}

#[wasm_bindgen]
pub fn log_message(msg: &str) {
    log(&format!("[Rust] {}", msg));
}

Ez lényegében deklarálja, hogy a console.log JavaScript funkció elérhető Rust oldalról is – innen már bármikor tudsz debug logolni a Rust kódodból.

Jó tudni

A wasm-bindgen verziója a Cargo.toml-ban és a CLI eszköz verziója (amit a wasm-pack húz be) mindig legyen kompatibilis egymással. Ha eltérés van, gyakran kriptikus fordítási vagy futásidejű hibát kapsz. Érdemes lefixálni a pontos verziót a Cargo.toml-ban.

Egyszerű interaktív komponens megírása és futtatása böngészőben

Nézzünk egy kicsit komplexebb példát: egy számlálót, amit egy gombra kattintva növelünk, és a Rust oldalon tartjuk az állapotot egy struct-ban.

use wasm_bindgen::prelude::*;
use std::cell::RefCell;

#[wasm_bindgen]
pub struct Counter {
    value: RefCell<i32>,
}

#[wasm_bindgen]
impl Counter {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Counter {
        Counter {
            value: RefCell::new(0),
        }
    }

    pub fn increment(&self) -> i32 {
        let mut v = self.value.borrow_mut();
        *v += 1;
        *v
    }

    pub fn get(&self) -> i32 {
        *self.value.borrow()
    }
}

Fontos, hogy a #[wasm_bindgen] által exportált struct-ok mezői nem lehetnek publikusak automatikusan hozzáférhetők JS oldalról – kizárólag a metódusokon keresztül kommunikálunk, ami egyébként jól illik a Rust encapsulation-filozófiájához.

JavaScript oldalon (moduláris importtal) így néz ki a használat, miután lefordítottuk a csomagot:

import init, { Counter } from "./pkg/hello_wasm.js";

async function run() {
  await init();
  const counter = new Counter();

  document.getElementById("btn").addEventListener("click", () => {
    const value = counter.increment();
    document.getElementById("output").textContent = value;
  });
}

run();

Az init() hívás tölti be a .wasm bináris fájlt aszinkron módon – ezt mindig meg kell várni (await), mielőtt a modul funkcióit használnánk.

Figyelem

Ne felejtsd el, hogy a Wasm modul betöltése aszinkron folyamat! Ha közvetlenül szinkron módon próbálod meg hívni a Rust funkciókat az init() promise feloldása előtt, undefined hibát vagy exception-t kapsz.

Build és csomagolás npm/webpack integrációhoz

A wasm-pack build parancs generálja a böngészőben (vagy Node.js-ben) használható csomagot. A leggyakoribb target-ek:

# Böngészőbarát ES modul, natívan importálható <script type="module">-ban
wasm-pack build --target web

# Bundler (webpack, rollup) számára optimalizált kimenet
wasm-pack build --target bundler

# Node.js CommonJS környezethez
wasm-pack build --target nodejs

A build eredménye a pkg/ mappában landol, benne a .wasm bináris, egy generált .js wrapper, TypeScript deklarációs fájl (.d.ts), és egy package.json, amit akár közvetlenül npm publish-elhetsz is.

Ha webpack projektbe szeretnéd integrálni, a package.json-ban egyszerűen hivatkozhatsz a lokális pkg mappára:

{
  "dependencies": {
    "hello-wasm": "file:../hello-wasm/pkg"
  }
}

Ezután a webpack konfigurációban engedélyezned kell a WebAssembly-modulok kezelését – ha webpack 5-öt használsz, ez alapból beépített funkcionalitás (experiments.asyncWebAssembly).

Tipp

Ha CI/CD pipeline-ban dolgozol, érdemes a wasm-pack build --release opciót használni, mert alapértelmezésben a debug build sokkal nagyobb és lassabb bináris fájlt eredményez.

Teljesítménybeli szempontok és hibakeresési tippek

A WebAssembly gyors, de van néhány buktató, amire figyelni kell:

Bináris méret. A Rust standard library egy része (pl. panic infrastruktúra, formázás) jelentős méretet ad a végleges .wasm fájlhoz. Csökkentheted ezt a következőkkel:

[profile.release]
opt-level = "z"      # méret-orientált optimalizáció
lto = true
codegen-units = 1
panic = "abort"       # elhagyja a panic unwind kódot

Adatátvitel a JS és Wasm között. A wasm-bindgen alapból stringeket és egyszerű típusokat automatikusan konvertál, de ez másolással jár. Ha nagy mennyiségű bináris adatot (pl. képpontokat) kell átadni, érdemes &[u8] szeletet vagy Vec<u8>-t használni, és kerülni a felesleges JSON szerializálást.

Debugolás. A console_error_panic_hook crate-tel a Rust panic-ok olvasható stack trace-ként jelennek meg a böngésző konzolján, panic helyett a kriptikus „unreachable executed” hibaüzenet helyett:

use wasm_bindgen::prelude::*;

#[wasm_bindgen(start)]
pub fn main() {
    console_error_panic_hook::set_once();
}

A #[wasm_bindgen(start)] attribútummal jelölt függvény automatikusan lefut a modul betöltésekor – kiváló hely az inicializáláshoz.

Megjegyzés

A let-else szintaxis (ami már stabil ebben a Rust verzióban) nagyon jól jön hibakezelő kódrészletekben, amikor egy Option vagy Result értéket akarsz kicsomagolni, és korai visszatérést akarsz, ha az sikertelen:

fn parse_input(s: &str) -> Option<i32> {
    let Ok(value) = s.parse::<i32>() else {
        return None;
    };
    Some(value * 2)
}

Összefoglalás

A Rust és a WebAssembly kombinációja izgalmas lehetőségeket nyit meg a frontend fejlesztésben: natív sebesség, típusbiztonság, és egy kiforrott eszközlánc (wasm-pack, wasm-bindgen), amivel viszonylag kevés fájdalommal illeszthetjük be a meglévő JavaScript/npm workflow-nkba. Nem kell az egész appot átírni – elég a számításigényes részeket kiszervezni Rust-ba, a maradékot pedig hagyni a JavaScript kényelmén. Ha még nem próbáltad, mostantól semmi nem akadályoz meg, hogy elindulj: telepítsd a wasm-pack-et, hozz létre egy cdylib crate-et, és nézd meg, mennyire egyszerű a Rust kódodat a böngészőbe csempészni.