lit-ssr-wasm

LitElement WASM Javy Go esbuild Compiled Mode Runtime Mode How it Works

Server-side render Lit web components from any language via WebAssembly

Two modes

Compiled mode

Component definitions are baked into a WASM module at build time. The module reads HTML from stdin and writes DSD-enhanced HTML to stdout. Fast and predictable -- ideal for static site generators, build pipelines, or any scenario where components are known ahead of time. The compiled mode demo on this site uses lit-ssr-demo.wasm, which has the example <my-alert> component baked in.

Runtime mode (lit-ssr-runtime.wasm)

No component definitions are baked in. The module uses a two-phase protocol: an init message sends the component JavaScript source and element tag names once, then each render sends NUL-terminated HTML. Consumers bundle Lit and their components using the provided esbuild plugins. The WASM runtime provides SSR shims (DOM, btoa, URL, Buffer, etc.) but not Lit APIs.

How it works

Both modes share the same core: @lit-labs/ssr and QuickJS bundled into a WASM module via esbuild and Javy. Node.js built-in modules are stubbed with lightweight shims (e.g. Buffer wraps TextEncoder/Uint8Array), since QuickJS does not provide them. The resulting .wasm file (~2 MB) communicates through WASI stdin/stdout, making it callable from any language with a WASI-compatible runtime -- Go, Rust, Python, or even the browser.

Why this works

Lit SSR deliberately avoids full DOM emulation. It intercepts Lit's template system at the string level, using a minimal DOM shim (@lit-labs/ssr-dom-shim) that provides just enough of the HTMLElement and customElements API for Lit's rendering logic. This means it runs comfortably in QuickJS -- no full browser engine needed.

Declarative Shadow DOM

The WASM module's output includes <template shadowrootmode="open"> elements containing the component's styles and rendered shadow DOM content. The browser attaches these as shadow roots during HTML parsing, before any JavaScript runs. Users see styled, laid-out content on first paint with zero layout shift.

Critically, the component's JavaScript definition is not needed on the page for DSD styles to apply. The compiled mode demo proves this: it loads lit-ssr-demo.wasm and uses its output via setHTMLUnsafe(), but never loads the <my-alert> element definition as JavaScript. The colored, styled alerts you see come entirely from Declarative Shadow DOM.

Running WASM in the browser

Both demos on this page run the actual WASM modules in the browser. A minimal inline WASI shim (~50 lines) maps the nine WASI preview 1 functions the module imports (fd_read, fd_write, proc_exit, etc.) to JavaScript buffers. No bundled JS is needed beyond the shim and a click handler -- the SSR engine runs entirely inside the WASM module.

Server-side usage

On the server, the WASM module is called from Go using wazero (a pure-Go WASM runtime with no CGo dependency). HTML is piped to stdin, DSD-enhanced HTML comes back on stdout. Cold start is ~350ms; subsequent renders ~320ms. The same module works with any WASI-compatible runtime (wasmtime, wasmer, etc.).