Language IntegrationsWebUI WebAssembly

WebUI WebAssembly #

WebUI provides browser-ready WebAssembly bindings through wasm-bindgen. The bindings are built as three variants so you can choose only the parser, only the handler, or the combined playground bundle.

Variants #

VariantImport pathExportsUse when
Handlerwasm/handler/webui_wasm_handler.jsProtocolYou already have protocol bytes and only need rendering
Parserwasm/parser/webui_wasm_parser.jsbuild_protocolYou need to compile virtual browser files into protocol bytes
Allwasm/all/webui_wasm_all.jsParser and handler exportsYou need both sides in one module, such as the docs playground

The handler-only bundle excludes webui-parser, and the parser-only bundle excludes webui-handler. The combined bundle keeps the previous playground behavior.

Building the WASM bundles #

cargo xtask build-wasm

The output is generated under docs/.webui-press/public/wasm/ for the documentation playground and release staging. Rebuild it when Rust code in webui-wasm, webui-parser, webui-handler, or webui-protocol changes.

Handler-only API #

Use the handler bundle when the protocol was built elsewhere and loaded as protobuf bytes in the browser.

import init, { Protocol } from './wasm/handler/webui_wasm_handler.js';

await init();

const protocolBytes = new Uint8Array(await (await fetch('/protocol.bin')).arrayBuffer());
const protocol = new Protocol(protocolBytes, 'webui');
const html = protocol.render(
  '{"title": "Hello"}',
  { entry: 'index.html', requestPath: '/' },
);

Keep the Protocol instance alive across renders. It decodes protobuf, builds deterministic indices, and binds the plugin once.

Protocol #

MethodDescription
render(stateJson, options?)Return complete rendered HTML as a string
renderStream(stateJson, onChunk, options?)Invoke callbacks coalesced around a 16 KiB target
streamResponse(entry, requestPath, options?)Open a progressive StreamingSession returning one Uint8Array per call
renderPartial(stateJson, entry, requestPath, inventoryHex)Return a complete JSON partial response with active-route projected state
renderComponentTemplates(componentTags, inventoryHex)Return requested template payloads and updated inventory
tokens()Return CSS token names in build order

For a complete static/CDN service worker example using this callback to write a ReadableStream and mirror --theme token injection in the browser, see Serverless Architecture.

StreamingSession #

streamResponse() opens a runtime-discovered session that maps directly onto a ReadableStream controller:

const session = protocol.streamResponse('index.html', '/');

const body = new ReadableStream({
  async start(controller) {
    let step = session.start(JSON.stringify(initialState));
    controller.enqueue(step.bytes);
    while (!step.done) {
      const boundary = step.boundary;
      if (boundary) {
        const state = await loadBoundaryState(
          boundary.owner,
          boundary.name,
          boundary.key,
        );
        step = session.resume(
          boundary.instanceId,
          JSON.stringify(state),
          'final',
        );
      } else {
        step = session.advance();
      }
      controller.enqueue(step.bytes);
    }
    controller.close();
  },
});
MemberDescription
start(stateJson)Return { bytes, done, boundary? } through the first occurrence or terminal
resume(instanceId, stateJson, mode?)Return only the pending occurrence's bytes through its checkpoint
advance()Return following parent bytes through the next occurrence or terminal
update(instanceId, patchJson)Return projected state bytes for an updatable occurrence

Descriptors contain instanceId, declarationId, owner, name, and an optional string or numeric key. A descriptor means resume; no descriptor with done: false means advance; done: true means complete. resume returns only the occurrence and checkpoint so it can be enqueued immediately. advance returns the following parent or tail bytes, with no sibling boundary workaround required. An update is valid between resume and advance. The final step's Uint8Array includes tail and terminal bytes. The contract is identical on Node, Python, C, and .NET.

Parser-only API #

Use the parser bundle when browser code needs to compile an in-memory file map into protocol bytes.

import init, { build_protocol } from './wasm/parser/webui_wasm_parser.js';

await init();

const files = {
  'index.html': '<h1>{{title}}</h1>',
  'my-card.html': '<template shadowrootmode="open"><p><slot></slot></p></template>',
  'my-card.css': 'p { color: red; }',
};

const protocolBytes = build_protocol(
  files,
  'index.html',
  [projectionManifest],
  'light',
);

build_protocol(files, entry, projectionManifests?, dom?) #

Parse virtual files into a WebUI protocol without rendering.

ParameterTypeDescription
filesRecord<string, string>Map of filenames to content
entrystringEntry HTML filename
projectionManifestsobject[]Optional bundler manifest fragments
dom"shadow" | "light"Optional unwrapped-component fallback; defaults to "shadow"

Returns protobuf-serialized WebUIProtocol as a Uint8Array. Throws on missing entry files, invalid templates, invalid component authoring, or protocol serialization errors.

Without manifests, initial and scripted navigation state remain full. With manifests, WASM applies the shared schema/build-ID validation, fragment merge, and strict coverage rules. Because virtual WASM builds have no filesystem, they cannot repeat disk stale-file checks and never analyze JavaScript.

Component discovery follows the virtual file map convention: HTML files with a hyphen in the name are registered as components, such as my-card.html for <my-card>. Matching .css files are paired and inlined with CssStrategy::Style.

Combined API #

Use the combined bundle when you want parser and handler exports from one module.

import init, { build_protocol, Protocol } from './wasm/all/webui_wasm_all.js';

await init();

const protocolBytes = build_protocol(files, 'index.html');
let html = '';
const protocol = new Protocol(protocolBytes);
protocol.renderStream(stateJson, (chunk) => {
  html += chunk;
}, { entry: 'index.html', requestPath: '/' });

The documentation playground imports this combined bundle and uses build_protocol followed by new Protocol(...) so it can measure compile and render time separately.

Differences from server-side rendering #

AspectServer (CLI / Rust / Node)WASM (Browser)
Protocol formatProtobuf binaryProtobuf bytes
CSS strategyLink by default, Style or Module when configuredStyle for virtual file builds
File I/OFilesystem and component discovery sourcesVirtual file map
StreamingSupported by native handlersProtocol.renderStream() calls a batched JavaScript callback, and Protocol.streamResponse() returns progressive chunks
Bundle choiceNative crates/addonsHandler-only, parser-only, or combined WASM