Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

This is a step-by-step tutorial to demonstrate how to run computations expressed with orx-parallel’s parallel iterators in a threaded WebAssembly browser app.

The project has three parts:

  1. computation/ contains ordinary Rust code and tests.
  2. wasm_bindings/ exposes the Rust functions to JavaScript.
  3. app/ contains the HTML, CSS, TypeScript, and Vite configuration. TypeScript client calls parallel computations through a worker.

The app starts one shared thread pool. threads: 0 lets the runtime choose the available capacity. Each computation also receives a thread count: 0 uses all initialized threads, while a positive value limits that computation.

Prerequisites

Install Rust and Cargo, Node.js and npm, and the wasm32-unknown-unknown Rust target:

rustup target add wasm32-unknown-unknown

Get started

Create your example application’s directory

mkdir par_wasm
cd par_wasm

The computation crate

Create the computation crate:

cargo new --lib computation
cd computation

orx-parallel dependency with wasm feature

Add orx-parallel dependency to implement parallel computations in par_wasm/computation/Cargo.toml:

[package]
name = "computation"
version = "0.1.0"
edition = "2024"
publish = false

[dependencies]
orx-parallel = { version = "4", default-features = false }

[features]
default = []
wasm = ["orx-parallel/wasm"]

Note that wasm feature is kept optional. This allows:

  • to use this crate as a regular Rust crate when the feature is omitted, and
  • to test the computations in isolation without WebAssembly dependencies.

Example computations

We will implement two parallel computations in par_wasm/computation/src/lib.rs as follows:

#![allow(unused)]
fn main() {
use orx_parallel::*;

fn fibonacci_term(index: usize) -> u64 {
	let mut previous = 0;
	let mut current = 1;

	for _ in 0..index {
		(previous, current) = (current, previous + current);
	}

	previous
}

pub fn calculate_fibonacci(workload: usize, num_threads: usize) -> u64 {
	(0..workload)
		.par()
		.num_threads(num_threads)
		.map(|index| fibonacci_term(index))
		.sum()
}

const MAX_MANDELBROT_ITERATIONS: u64 = 10000;

fn mandelbrot_escape_iterations(point_index: usize, workload: usize) -> u64 {
	let width = (workload as f64).sqrt().ceil() as usize;
	let height = workload.div_ceil(width);
	let x = point_index % width;
	let y = point_index / width;

	let real = -2.0 + 3.0 * x as f64 / width.saturating_sub(1).max(1) as f64;
	let imaginary = -1.5 + 3.0 * y as f64 / height.saturating_sub(1).max(1) as f64;
	let (mut z_real, mut z_imaginary) = (0.0, 0.0);

	for iteration in 1..=MAX_MANDELBROT_ITERATIONS {
		(z_real, z_imaginary) = (
			z_real * z_real - z_imaginary * z_imaginary + real,
			2.0 * z_real * z_imaginary + imaginary,
		);

		if z_real * z_real + z_imaginary * z_imaginary > 4.0 {
			return iteration;
		}
	}

	MAX_MANDELBROT_ITERATIONS
}

pub fn mandelbrot_checksum(workload: usize, num_threads: usize) -> u64 {
	(0..workload)
		.par()
		.num_threads(num_threads)
		.map(|point_index| mandelbrot_escape_iterations(point_index, workload))
		.sum()
}

#[cfg(test)]
mod tests {
	use super::*;

	#[test]
	fn calculates_fibonacci_terms() {
		assert_eq!(calculate_fibonacci(6, 2), 12);
	}

	#[test]
	fn calculates_mandelbrot_checksum() {
		assert_eq!(mandelbrot_checksum(4, 2), 6);
	}
}
}

Computations are just examples using orx-parallels parallel iterator. Briefly, calculate_fibonacci maps independent Fibonacci terms and sums them. mandelbrot_checksum maps points, calculates escape iterations, and sums the results.

.num_threads(num_threads) lets the caller control the thread limit per-computation. Omitting the .num_threads call or calling it with 0 allows to use all threads available in the pool.

Test this crate before defining WASM bindings:

cargo test

One level up into par_wasm directory:

cd ..

The WASM bindings crate

Create a thin layer to define WebAssembly bindings:

cargo new --lib wasm_bindings
cd wasm_bindings

Dependencies

We will add dependencies to:

  • wasm-bindgen for creating WebAssembly bindings, and
  • to our own computation crate using the wasm feature.

Recall that wasm feature of computation crate enables the wasm feature of orx-parallel.

Update par_wasm/wasm_bindings/Cargo.toml as follows:

[package]
name = "wasm_bindings"
version = "0.1.0"
edition = "2024"
publish = false

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

[dependencies]
computation = { path = "../computation", features = ["wasm"] }
wasm-bindgen = "0.2"

Exposed functions

Update par_wasm/wasm_bindings/src/lib.rs as follows:

#![allow(unused)]
fn main() {
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn calculate_fibonacci(workload: u32, num_threads: u32) -> u64 {
    computation::calculate_fibonacci(workload as usize, num_threads as usize)
}

#[wasm_bindgen]
pub fn mandelbrot_checksum(limit: u32, num_threads: u32) -> u32 {
    computation::mandelbrot_checksum(limit as usize, num_threads as usize) as u32
}
}

Notice that we keep this layer as thin as possible:

  • we make necessary type conversions,
  • call our computation crate functions.

Build (optional)

You may try building this crate before implementing the frontend:

RUSTUP_TOOLCHAIN=nightly \
CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUSTFLAGS='-C target-feature=+atomics -C link-arg=--shared-memory -C link-arg=--max-memory=1073741824 -C link-arg=--import-memory -C link-arg=--export=__heap_base -C link-arg=--export=__wasm_init_tls -C link-arg=--export=__tls_size -C link-arg=--export=__tls_align -C link-arg=--export=__tls_base' \
cargo build \
  --target wasm32-unknown-unknown \
  --release \
  -Z build-std=panic_abort,std

These flags enable multi-threaded WebAssembly execution: +atomics and --shared-memory enable atomic operations and shared linear memory for thread coordination, --max-memory sets the memory limit, --import-memory allows the host to provide memory, and the __* exports expose thread-local storage (TLS) setup functions needed for proper thread initialization.

As we will see in the next section, this build step will be automated using orx-parallel-wasm.

One level up into par_wasm directory:

cd ..

The vanilla app

Create the directory for the browser app:

mkdir app
cd app

Configuration

package.json

Create par_wasm/app/package.json as follows:

{
    "name": "par-wasm-app",
    "version": "0.1.0",
    "private": true,
    "type": "module",
    "scripts": {
        "build:wasm": "ORX_PARALLEL_WASM_BINDINGS=../wasm_bindings ORX_PARALLEL_WASM_OUT_DIR=./pkg node ./node_modules/orx-parallel-wasm/dist/build.js build",
        "dev": "npm exec -- vite",
        "typecheck": "tsc --noEmit",
        "build": "npm run build:wasm && npm run typecheck && npm exec -- vite build"
    },
    "dependencies": {
        "orx-parallel-wasm": "git+https://github.com/orxfun/orx-parallel-wasm.git"
    },
    "devDependencies": {
        "typescript": "^5.6.3",
        "vite": "^5.4.10"
    }
}

The dependencies section installs orx-parallel-wasm, which provides the ParallelWorker client and the Vite integration used below.

The scripts keep the build reproducible:

  • build:wasm compiles the sibling wasm_bindings crate into pkg,
  • typecheck checks the TypeScript source,
  • build runs both steps and then creates the production bundle, and
  • dev starts Vite’s development server.

tsconfig.json

Create par_wasm/app/tsconfig.json as follows:

{
    "compilerOptions": {
        "target": "ES2020",
        "module": "ESNext",
        "moduleResolution": "Bundler",
        "strict": true,
        "isolatedModules": true,
        "skipLibCheck": true,
        "types": [
            "vite/client"
        ]
    },
    "include": [
        "src"
    ]
}

This is a small, otherwise standard TypeScript configuration for a Vite application. moduleResolution: "Bundler" lets TypeScript resolve Vite-style imports such as ?url, strict enables type checking, and types: ["vite/client"] supplies Vite’s client-side type declarations. Only the src directory is typechecked.

vite.config.ts

Create par_wasm/app/vite.config.ts as follows:

import { defineConfig } from "vite";
import { orxParallelWasm } from "orx-parallel-wasm/vite";

export default defineConfig({
    base: "./",
    plugins: [
        orxParallelWasm({
            bindings: "../wasm_bindings"
        })
    ],
    server: {
        headers: {
            "Cross-Origin-Opener-Policy": "same-origin",
            "Cross-Origin-Embedder-Policy": "require-corp"
        }
    },
    worker: {
        format: "es"
    }
});

The plugin compiles the sibling bindings crate and writes generated files to pkg.

The two server headers enable SharedArrayBuffer, which is required by threaded WebAssembly.

worker.format makes the generated worker an ES module.

Page markup

Create par_wasm/app/index.html:

<!doctype html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>orx-parallel WASM mini tutorial</title>
</head>

<body>
    <main>
        <p class="eyebrow">orx-parallel / WebAssembly</p>
        <h1>Parallel computations using one shared thread pool</h1>
        <p class="intro">Run <code>orx-parallel</code> computations with different worker counts.</p>

        <section class="panel" aria-labelledby="settings-title">
            <h2 id="settings-title">Run settings</h2>
            <label>
                Threads
                <input id="threads" type="number" value="0" min="0" step="1" />
                <span id="threads-help">0 uses all initialized threads</span>
            </label>
            <p id="pool-status" role="status">Initializing thread pool...</p>
        </section>

        <section class="computations" aria-label="Computations">
            <article class="computation">
                <p class="index">01</p>
                <h2>Fibonacci workload</h2>
                <p>Sum many Fibonacci terms to give each worker useful CPU work.</p>
                <label>
                    Number of terms
                    <input id="fibonacci-workload" type="number" value="50000" min="1" step="1000" />
                </label>
                <button id="run-fibonacci" type="button">Calculate Fibonacci</button>
                <p id="fibonacci-result" class="result">No result yet.</p>
            </article>

            <article class="computation">
                <p class="index">02</p>
                <h2>Mandelbrot checksum</h2>
                <p>Calculate a checksum across a configurable number of Mandelbrot points.</p>
                <label>
                    Number of points
                    <input id="mandelbrot-workload" type="number" value="50000" min="1" step="1000" />
                </label>
                <button id="run-mandelbrot" type="button">Calculate Checksum</button>
                <p id="mandelbrot-result" class="result">No result yet.</p>
            </article>
        </section>
    </main>
    <script type="module" src="./src/main.ts"></script>
</body>

</html>

Styling

Create par_wasm/app/style.css:

:root {
    color: #17221f;
    background: #e8eee8;
    font-family: Georgia, "Times New Roman", serif;
    font-synthesis: none;
}

* {
    box-sizing: border-box;
}

body {
    margin: 0;
    background: linear-gradient(135deg, #e8eee8 0%, #f6f1e8 52%, #d7e4e0 100%);
}

main {
    max-width: 1080px;
    margin: 0 auto;
    padding: 8vh 6vw 10vh;
}

.eyebrow,
.index {
    color: #b34b2d;
    font: 700 0.78rem/1.2 Arial, sans-serif;
    letter-spacing: 0.08em;
    text-transform: uppercase;
}

h1 {
    max-width: 760px;
    margin: 1rem 0;
    font-size: clamp(1.4rem, 3.5vw, 3.25rem);
    line-height: 0.92;
    font-weight: 400;
}

.intro {
    max-width: 560px;
    color: #4d5d57;
    font-size: 1.2rem;
    line-height: 1.5;
}

.panel {
    margin: 4rem 0 2rem;
    padding: 1.5rem;
    border-top: 2px solid #17221f;
    border-bottom: 1px solid #9eaea5;
}

h2 {
    margin: 0.4rem 0 0.7rem;
    font-size: 1.55rem;
    font-weight: 400;
}

label {
    display: grid;
    gap: 0.45rem;
    color: #4d5d57;
    font: 700 0.78rem/1.2 Arial, sans-serif;
    text-transform: uppercase;
}

input {
    width: 100%;
    padding: 0.75rem;
    border: 1px solid #9eaea5;
    border-radius: 2px;
    color: #17221f;
    background: #fffdf7;
    font: 1rem Georgia, serif;
}

#threads {
    max-width: 12rem;
}

#threads-help,
#pool-status {
    color: #65766e;
    font: 0.85rem Arial, sans-serif;
}

.computations {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 1.5rem;
}

.computation {
    padding: 1.6rem;
    border: 1px solid #9eaea5;
    background: rgba(255, 253, 247, 0.72);
}

.computation p:not(.index) {
    color: #65766e;
    line-height: 1.45;
}

.computation label {
    margin: 1.5rem 0;
}

button {
    padding: 0.8rem 1rem;
    border: 0;
    border-radius: 2px;
    color: #fffdf7;
    background: #b34b2d;
    font: 700 0.8rem Arial, sans-serif;
    text-transform: uppercase;
    cursor: pointer;
}

button:hover {
    background: #8f3924;
}

button:disabled {
    cursor: wait;
    opacity: 0.55;
}

.result {
    min-height: 2.8rem;
    margin-bottom: 0;
    font-family: Arial, sans-serif;
}

@media (max-width: 700px) {
    main {
        padding: 2.5rem 1.25rem 5rem;
    }

    .computations {
        grid-template-columns: 1fr;
    }

    h1 {
        font-size: 2rem;
    }
}

TypeScript client

We are ready to create the Typescript client where we will create the thread pool and call exposed parallel computations.

Create par_wasm/app/src/main.ts:

import { ParallelWorker } from "orx-parallel-wasm";
import bindingsUrl from "../pkg/wasm_bindings.js?url";
import "../style.css";

// Desired number of threads in the thread pool, if the hardware allows.
// Setting it to 0 allows using all available threads.
const THREADS_IN_POOL = 0;

type Computations = {
    calculate_fibonacci: (workload: number, threads: number) => bigint;
    mandelbrot_checksum: (limit: number, threads: number) => number;
};

// Create worker with exported parallel, or sequential, computations
const worker = new ParallelWorker<Computations>({
    bindingsUrl,
    methods: ["calculate_fibonacci", "mandelbrot_checksum"],
    threads: THREADS_IN_POOL
});

const ui = {
    threads: document.querySelector<HTMLInputElement>("#threads")!,
    threadsHelp: document.querySelector<HTMLSpanElement>("#threads-help")!,
    poolStatus: document.querySelector<HTMLParagraphElement>("#pool-status")!,
    fibonacciWorkload: document.querySelector<HTMLInputElement>("#fibonacci-workload")!,
    mandelbrotWorkload: document.querySelector<HTMLInputElement>("#mandelbrot-workload")!,
    runFibonacci: document.querySelector<HTMLButtonElement>("#run-fibonacci")!,
    runMandelbrot: document.querySelector<HTMLButtonElement>("#run-mandelbrot")!,
    fibonacciResult: document.querySelector<HTMLParagraphElement>("#fibonacci-result")!,
    mandelbrotResult: document.querySelector<HTMLParagraphElement>("#mandelbrot-result")!
};

// Per-computation thread limit.
// Setting it to 0 allows using all threads in the thread pool.
function readThreads(): number {
    const value = Number.parseInt(ui.threads.value, 10);
    const maxThreads = worker.initializedThreads ?? 1;
    const threads = Number.isFinite(value) ? Math.max(0, Math.min(maxThreads, value)) : 0;
    ui.threads.value = String(threads);
    return threads;
}

function readPositive(input: HTMLInputElement): number {
    const value = Number.parseInt(input.value, 10);
    return Number.isFinite(value) ? Math.max(1, value) : 1;
}

async function run<T>(button: HTMLButtonElement, output: HTMLParagraphElement, computation: () => Promise<T>): Promise<void> {
    button.disabled = true;
    output.textContent = "Running...";
    const startedAt = performance.now();

    try {
        const result = await computation();
        const elapsed = performance.now() - startedAt;
        output.textContent = `Result: ${String(result)} | ${elapsed.toFixed(2)} ms`;
    } catch (error) {
        output.textContent = `Error: ${error instanceof Error ? error.message : String(error)}`;
    } finally {
        button.disabled = false;
    }
}

void worker.ready().then(
    () => {
        ui.threads.max = String(worker.initializedThreads);
        ui.threadsHelp.textContent = `0 uses all ${worker.initializedThreads} initialized threads`;
        ui.poolStatus.textContent = `Thread pool ready: ${worker.initializedThreads} threads`;
    },
    (error: unknown) => {
        ui.poolStatus.textContent = `Thread pool error: ${error instanceof Error ? error.message : String(error)}`;
        ui.runFibonacci.disabled = true;
        ui.runMandelbrot.disabled = true;
    }
);

ui.runFibonacci.addEventListener("click", () => {
    void run(ui.runFibonacci, ui.fibonacciResult, () =>
        worker.call("calculate_fibonacci", [readPositive(ui.fibonacciWorkload), readThreads()])
    );
});

ui.runMandelbrot.addEventListener("click", () => {
    void run(ui.runMandelbrot, ui.mandelbrotResult, () =>
        worker.call("mandelbrot_checksum", [readPositive(ui.mandelbrotWorkload), readThreads()])
    );
});

window.addEventListener("beforeunload", () => worker.terminate());

The generated pkg/wasm_bindings.js import is intentionally present before the first build, the build:wasm script creates it.

The ParallelWorker is the bridge between the page and the Rust WASM module:

  • bindingsUrl points to the generated bindings package,
  • methods lists the exported Rust functions that the worker may call,
  • and THREADS_IN_POOL: 0 asks the runtime to size the shared pool automatically.

The worker initializes that pool when ready() resolves and exposes the resulting capacity through initializedThreads.

Each computation is invoked with worker.call(method, arguments):

  • The method name must be one of the names listed in methods, and the arguments must match the corresponding #[wasm_bindgen] function.
  • For example, the Fibonacci button sends the workload and selected thread count to calculate_fibonacci;
  • the Mandelbrot button does the same for mandelbrot_checksum.

The shared run() helper disables the active button, waits for the worker result, and displays the result and elapsed time.

One level up into par_wasm directory:

cd ..

Build and run

This tutorial uses Vite for the frontend build because its configuration is compact and its development server makes the required cross-origin isolation headers easy to configure. The same orx-parallel-wasm package also provides integrations for Webpack, Rspack, and Rollup; the next section links to equivalent examples and explains what those integrations handle.

At this point the project has this source layout:

par_wasm/
├── app/
│   ├── index.html
│   ├── package.json
│   ├── style.css
│   ├── tsconfig.json
│   ├── vite.config.ts
│   └── src/main.ts
├── computation/
│   ├── Cargo.toml
│   └── src/lib.rs
└── wasm_bindings/
	├── Cargo.toml
	└── src/lib.rs

The lock files are generated by Cargo and npm.

The pkg/ and dist/ directories are generated during the build.

Build

From the app directory (par_wasm/app), install the npm dependency and build the app:

cd app
npm install
npm run build

The build script performs three steps:

  1. build:wasm invokes orx-parallel-wasm with wasm_bindings/ as its input and writes the generated JavaScript and WebAssembly files to app/pkg.
  2. typecheck runs TypeScript without emitting JavaScript.
  3. Vite bundles the page, TypeScript, generated WASM module, and worker into app/dist.

The Vite plugin compiles the bindings for wasm32-unknown-unknown with atomics and shared memory enabled. The first build may download the WASM tooling used by the package.

The development server sends these headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

They are required for SharedArrayBuffer and browser threads.

Run

Start Vite from the same directory:

npm run dev

Open the URL printed by Vite and try it out with different number of threads:

  • The page reports the number of initialized threads when the worker is ready.
  • If initialization fails, check that both cross-origin isolation headers are present; opening index.html directly does not provide them.

Source code of the app built by following this tutorial can be found here.

Other bundlers

The app built in this tutorial uses Vite, but the WASM bindings can be used with other JavaScript bundlers. The orx-parallel-wasm package provides a bundler-specific plugin for each integration:

Each example is the same small application with a different bundler configuration. The plugin is imported from the corresponding subpath, such as orx-parallel-wasm/vite or orx-parallel-wasm/webpack.

Why the plugins exist

A Rust crate compiled for browser threads is more involved than importing a single .wasm file. The plugin coordinates the build and packaging steps that connect Rust, WebAssembly, JavaScript, and the bundler:

  • it invokes the WASM build for the bindings crate with the required wasm32-unknown-unknown, atomics, and shared-memory settings;
  • it prepares the wasm-bindgen JavaScript and worker code so the browser can create the parallel worker correctly;
  • it emits the generated WASM and JavaScript as normal bundler assets, including a stable bindings entry for the application; and
  • it provides or records the Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers required by SharedArrayBuffer.

Without a plugin, these steps would need to be kept in sync with the bundler’s hooks, asset graph, output directory, worker handling, and development server. The details differ between bundlers, which is why orx-parallel-wasm exposes separate integrations rather than one generic configuration.

Rollup is intentionally more low-level than Vite, Webpack, or Rspack. Its example also copies the HTML and CSS files and configures a development server, because Rollup does not provide those pieces by itself.

Building it yourself

It is possible to build the application without one of these plugins. The essential workflow is:

  1. Run the WASM build as a separate step, targeting wasm32-unknown-unknown with atomics and shared memory enabled (see the command in The WASM bindings crate).
  2. Choose a known public or bundled location for the generated bindings, WASM module, and worker assets.
  3. Configure the bundler to copy or emit those assets without changing the URLs expected by the generated worker code.
  4. Initialize ParallelWorker with the URL of the generated bindings entry.
  5. Configure the development server to send the required cross-origin isolation headers.
  6. Configure the production server or hosting platform to send the same headers when serving the built application.

A complete plugin-free version of this demo is available in the vanilla-manual example. It uses the bundler-neutral build command, esbuild only to bundle the application TypeScript, and a small Node.js server to serve the generated assets with the required headers.

The plugin-free example adds two small files to take over the integration work:

  • build.mjs runs the bundler-neutral orx-parallel-wasm build command, copies the generated bindings, WASM module, and worker assets into dist, and bundles the application TypeScript with esbuild.
  • server.mjs serves dist and adds the Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp headers required by SharedArrayBuffer.

The orx-parallel-wasm package is still used. It provides the WASM build and preparation logic as well as the ParallelWorker runtime client; the application only supplies the bundler and server wiring that a plugin would otherwise provide.

This approach gives more control over output names, caching, deployment, and bundler behavior, but it also makes the integration the application’s responsibility. The examples above are useful references when implementing that workflow manually or when adapting the plugin to a different build system.

Other frameworks

This tutorial uses vanilla JavaScript and TypeScript to keep the browser-facing code as small and transparent as possible. The computation crate, WASM bindings crate, worker boundary, and browser requirements do not depend on that choice of UI framework. React can use the same structure with components and state managing the page instead of direct DOM updates.

React with Vite

The React + Vite mini example provides the same Fibonacci and Mandelbrot demo using React. Its computation/ and wasm_bindings/ crates are identical to the ones used by the other examples/wasm/mini projects, and its stylesheet and output match the vanilla example.

The worker is created in the React entrypoint in the same way as in the vanilla app: it is given the generated bindings URL, the exported method names, and the desired thread count. The worker instance is then passed to the App component as a prop. App uses React state for input values, status messages, results, and button state, while calls still cross the same ParallelWorker boundary.

This separation is useful in a larger application: React owns rendering and UI state, while ParallelWorker owns communication with the module worker and the WASM bindings. The computation itself remains in Rust.

Rust UI frameworks

The TSP examples include additional applications built with different frontend approaches:

  • Vanilla TypeScript uses Vite and direct DOM updates.
  • React uses React components with a Vite host application.
  • Yew uses a Rust Yew component crate hosted by a Vite browser application.
  • Leptos uses a Rust Leptos component crate with a Vite browser host.

The Vanilla and React examples keep the UI in JavaScript or TypeScript. The Yew and Leptos examples move the UI into Rust and compile it to WASM, but the architecture is still recognizable: a computation crate contains the algorithm, a bindings boundary exposes the WASM API, and the browser application owns initialization, worker lifecycle, and cross-origin isolation.

For the published demos, see the TSP example hub. These examples are larger than the mini tutorial, but they demonstrate that the same parallel WASM design can be adapted to vanilla JavaScript, React, Yew, or Leptos without changing the core computation model.