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

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