Skip to main content

Vite example

This example shows how to use rust-wasmpack-loader with Vite. The loader ships a Vite plugin, so you import .rs files directly and the plugin compiles each one to the strategy that matches where it runs: the node strategy for the server (SSR) and the web strategy for the client, automatically.

Because Vite plugins are a superset of Rollup plugins, the Rollup plugin also works inside Vite with a single fixed target. The dedicated Vite plugin shown here adds the SSR/client split on top of that.

How delivery is decided​

The plugin chooses where the WebAssembly comes from per build:

BuildStrategyWASM delivery
Client production build (vite build)webEmitted as a separate .wasm asset and fetched at runtime
Client dev (vite / vite serve)webInlined as bytes
Any SSR build or devnodeInlined as bytes
ssr.target webworker/edgenodeInlined as bytes

Only the client production build benefits from a separate asset, so that is the one case where the plugin emits one. Everywhere else it inlines the bytes, which is always correct and needs no asset plumbing.

Server vs client is detected per module: the plugin reads this.environment.config.consumer on Vite 6+ and falls back to the options.ssr flag on Vite 5.

Project structure​

vite-example/
β”œβ”€β”€ src/
β”‚ β”œβ”€β”€ index.client.js # Client entry (web strategy)
β”‚ β”œβ”€β”€ index.ssr.js # SSR entry (node strategy)
β”‚ β”œβ”€β”€ index.test.js # Test driving both builds with node:test
β”‚ └── lib.rs # Rust WebAssembly code
β”œβ”€β”€ dist-client/ # Client build output
β”œβ”€β”€ dist-ssr/ # SSR build output
β”œβ”€β”€ vite.config.mjs # Vite configuration wiring up the plugin
β”œβ”€β”€ Cargo.toml # Rust configuration
└── package.json # Dependencies and scripts

Setup​

1. Initialize the project​

mkdir my-vite-wasm-app
cd my-vite-wasm-app
npm init -y

2. Install dependencies​

npm install --save-dev rust-wasmpack-loader vite

3. Create Cargo.toml​

Cargo.toml
[package]
name = "vite-wasm-example"
version = "0.1.0"
edition = "2021"

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

[dependencies]
wasm-bindgen = "0.2.95"

4. Create the Rust code​

src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {}! Greetings from Rust via Vite!", name)
}

5. Create the entries​

The same .rs import works in both entries; the plugin decides the strategy.

src/index.ssr.js
import wasmModule from "./lib.rs";

// On the server `wasmModule` resolves synchronously (bytes are inlined).
console.log(wasmModule.greet("Vite Developer"));
console.log(`fibonacci(10) = ${wasmModule.fibonacci(10)}`);
src/index.client.js
import wasmModule from "./lib.rs";

// In the client production build `wasmModule` is a Promise: the wasm is fetched
// from the emitted asset, so await it before use.
wasmModule.then((rs) => {
console.log(rs.greet("Vite Developer"));
console.log(`fibonacci(10) = ${rs.fibonacci(10)}`);
});

6. Create the Vite config​

vite.config.mjs
import { defineConfig } from "vite";
import rustWasmLoader from "rust-wasmpack-loader";

export default defineConfig({
plugins: [rustWasmLoader.vite()],
build: {
// Keep the emitted .wasm as a separate asset on the client build instead
// of letting Vite inline small assets as base64.
assetsInlineLimit: 0,
},
});

7. Update package.json​

package.json
{
...,
"type": "module",
"scripts": {
"build": "vite build",
"build:ssr": "vite build --ssr src/index.ssr.js"
},
...
}

Running the example​

# Client production build (emits a separate .wasm asset)
npm run build

# SSR build (inlines the wasm; run the output with node)
npm run build:ssr

Plugin options​

rustWasmLoader.vite({
// Package name(s) shipping .rs files to keep bundled for SSR.
ssrNoExternal: [],
logLevel: "info", // "verbose" | "info" | "warn" | "error" | "quiet"
});

Crates from node_modules (SSR)​

When a .rs file lives inside an installed package, Vite externalizes that package for SSR by default and the plugin never sees the import. Vite matches ssr.noExternal against the npm package name, not the file extension, so the plugin cannot detect .rs-bearing dependencies on its own. Name the package through ssrNoExternal to keep it bundled:

rustWasmLoader.vite({ ssrNoExternal: ["my-rust-crate-package"] });

The plugin merges those names into ssr.noExternal for you. A .rs file in your own source tree needs no extra configuration.


SSR and client from one import

The same import wasmModule from "./lib.rs" compiles to the node strategy on the server and the web strategy on the client. Remember that the client production build resolves to a Promise (the wasm is fetched), while SSR resolves synchronously (the bytes are inlined).