Server-side rendering of Stencil web components from PHP, Python, Ruby, Go, Rust, Java and C#.
Stencil's ssr-wasm output target builds a component
library's server renderer as a WebAssembly Extism plugin. Any server with an
Extism SDK can load it and render the components to HTML, without Node.js. The browser then
hydrates that HTML with the library's normal client bundle.
Each screenshot is the HTML a different language's server sent, with JavaScript disabled in the browser.
![]() PHP / Laravel |
![]() Python / Django |
![]() Ruby / Rails |
![]() Go / net/http |
![]() Rust / Axum |
![]() Java / Spring Boot |
![]() C# / ASP.NET Core |
Add the ssr-wasm output target next to your browser bundle, build, and give server developers
the .wasm file and the browser bundle. Most components work unchanged. The main difference is
that object and array props arrive as JSON attributes: declare them with
@Prop({ serialize: 'json' }).
packages/components is a working library set up this way, and its README
covers what to ship and how to write components for it.
Pick the example for your stack. Each one is a small app showing both ways to use the plugin, with a README on how it works and what to watch out for:
| Example | Framework | Extism SDK |
|---|---|---|
| PHP | Laravel | extism/extism |
| Python | Django | extism |
| Ruby | Rails | extism gem |
| Go | net/http + html/template |
extism/go-sdk |
| Rust | Axum + MiniJinja | extism crate |
| Java | Spring Boot + Thymeleaf | org.extism.sdk:extism |
| C# | ASP.NET Core | Extism.Sdk |
- Load the plugin once. Compile
index.wasmwith WASI enabled, once per process. A plugin instance isn't thread-safe, so give each worker or thread its own instance, or use a pool. - Render. Call
renderToStringwith{"html": "...", "options": {...}}. It returns JSON, with the rendered HTML inhtml. If the result has__errorinstead, log it and serve the HTML you passed in: the components then render in the browser. - Choose how much HTML to send. Every example shows both:
- Middleware (
/): render the whole page as usual, then pass the finished HTML throughrenderToString. It's the smallest change to an existing app. - Template helper (
/helper): a template tag or helper renders single components with"fullDocument": false. Only those components go through the plugin.
- Middleware (
- Reset hydration IDs once per request when using fragments: call
resetSsrDocDatabefore the request's first fragment, never between fragments of one page. Whole-page renders don't need it. - Serve the browser bundle and load it on the page, so the components hydrate.
Median render times on a warm server, in a small Docker VM (1 CPU, ~2.5 GB), @stencil/core
5.0.0-beta.35. "Plugin" is time spent in renderToString; "end to end" is the whole HTTP
response. Treat them as rough and relative.
| Example | Extism runtime | / plugin |
/ end to end |
/helper plugin |
/helper end to end |
|---|---|---|---|---|---|
| PHP / Laravel | wasmtime | 46 ms | 59 ms | ||
| Python / Django | wasmtime | 40 ms | 42 ms | 50 ms | 53 ms |
| Ruby / Rails | wasmtime | 45 ms | 57 ms | ||
Go / net/http |
wazero | 133 ms | 135 ms | 162 ms | 164 ms |
| Rust / Axum | wasmtime | 36 ms | 38 ms | 42 ms | 44 ms |
| Java / Spring Boot | wasmtime | 47 ms | 58 ms | 56 ms | 71 ms |
| C# / ASP.NET Core | wasmtime | 36 ms | 38 ms | 47 ms | 51 ms |
- The Extism runtime matters more than the language. The plugin spends most of its time in QuickJS, a JavaScript interpreter. wasmtime runs it several times faster than wazero, which is why Go is slower.
- Node is faster still. The same page renders in about 9 ms with Stencil's native
ssroutput target in Node, in the same VM (4 ms withreuseWindow). If your server is Node already, use that instead. - 40–60 ms per page is still a normal server-rendering budget. The output can be cached like any other template output.
Requirements: Node 22+, pnpm, Docker, and
extism-js (plus Binaryen's wasm-merge
and wasm-opt) on your PATH.
pnpm install
pnpm build # builds the component library and its plugin
scripts/docker-build.sh python-django # build one example's image (or omit the name for all)
docker run --rm -p 8002:8002 ssr-wasm-python-django
open http://localhost:8002The images copy in packages/components/dist, so run pnpm build first. Each example's README
also covers running it without Docker. Behind a TLS-inspecting proxy, set
EXTRA_CA_CERT=/path/to/proxy-root-ca.pem before running scripts/docker-build.sh.
Every example serves the same pages, described in CONTRACT.md, and one test suite
in tests/conformance checks them all: the server HTML, the page with
JavaScript off, and hydration with JavaScript on.
pnpm test # against a minimal Node implementation of CONTRACT.md
BASE_URL=http://localhost:8002 pnpm test # against a running example
scripts/conformance-all.sh # every example's image, one after anotherTo add an example, implement CONTRACT.md, give it the next port, add it to
scripts/conformance-all.sh, and check it with the suite.






