Server-side render Lit web components from any language via WebAssembly
Two modes
Builtin mode (lit-ssr-builtin.wasm)
Component definitions are baked into the 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.
echo '<my-alert type="success">Done</my-alert>' | wasmtime lit-ssr-builtin.wasm
Runtime mode (lit-ssr-runtime.wasm)
No component definitions are baked in. The module reads JSON from stdin
containing the component JavaScript source, HTML to render, and element
tag names. Lit APIs (LitElement, html,
css, classMap, etc.) are exposed as globals
inside the QuickJS context, so user-provided source does not need import
statements. The source is evaluated internally by QuickJS, registering
custom elements, then the HTML is rendered with DSD.
echo '{"source":"...","html":"...","elements":["my-alert"]}' | wasmtime lit-ssr-runtime.wasm
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-builtin.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.).