post-renderer · Rust → WebAssembly
Experimental · a for-fun prototype · not yet open source

A browser engine
that fits inside
a sandbox.

Prism takes an interactive post written as ordinary HTML, CSS and JavaScript, then parses, styles, lays out, scripts, paints and hit-tests it entirely inside WebAssembly. The browser never sees the markup — it gets back only pixels and a short list of actions.

SCROLL TO TRACE A FRAME  
<html>.css.js
RGBA pixels+ actions
PLAYABLE · NOT A VIDEO

Don't take my word for it — play it.

This Flappy Bird is running inside the same WebAssembly engine: its HTML, CSS and JavaScript are parsed, laid out, scripted and painted in Rust — the browser just blits the pixels. Click the board or press Space to flap.

● booting WASM…
The core idea

Source goes in. Pixels come out.

Everything a browser would normally do with untrusted markup happens on the far side of a hard boundary — and only a framebuffer crosses back.

Browser host (JS / React)

  • Fetches fixture HTML + declared assets
  • new Renderer(html, w, h, dpr)
  • Drives tick() every animation frame
  • Blits returned RGBA with putImageData
  • Forwards pointer & key events
  • Performs actions: navigation, byte-range fetches

Prism runtime (Rust / WASM)

  • Parses HTML into a private node arena
  • Resolves the CSS cascade & lays out boxes
  • Runs author JS in a locked-down QuickJS engine
  • Shapes text, decodes images & video
  • Paints to a pixel buffer and hit-tests input
  • Emits actions through a one-way queue
LIVE · THE ACTUAL ENGINE

This panel is the real renderer.

The canvas below is the genuine article — the prebuilt post_renderer.wasm parses this fixture, resolves its CSS, runs taffy layout, executes the QuickJS script and rasterizes with tiny-skia, all in WebAssembly. Drag the slider: pointer coordinates go in, pixels come out. Everything further down is an illustration of internals the single WASM binary doesn't expose — this is the only panel that's actually the engine.

● booting WASM…
post_renderer.wasm · the real binary · ~1.7 MB over the wirenew Renderer → tick() → paint()
One frame · end to end

Fly through a single frame.

Scroll to ride one frame the whole way around the loop — out of the browser, down through every layer of the WASM renderer to a single pixel, and back to the glass. The ring tracks the full circle; the gauge shows how deep you are.

FLY THROUGH THE FRAME
STAGE 01 / PARSE

HTML becomes a node arena.

html5ever tokenizes the markup; Prism copies it into a flat SlotMap arena where every node has a stable NodeId. Hover the source or the arena to see the mapping — there is no live DOM, just cells Prism owns.

interactive · hover to link source ↔ arena
html5ever · tokenize + tree-buildslotmap · arena storage
STAGE 02 / STYLE

Folding the cascade into one style.

lightningcss parses every rule; Prism matches selectors and collapses them — by specificity — into a single ComputedStyle per node. Toggle the rules and watch the winner change.

interactive · click rules to toggle them
Yeti
lightningcss · parse + cascadeComputedStyle · ~5,000 lines of subset
STAGE 03 / FONTS & TEXT

Outlines become alpha pixels.

Three typefaces are baked into the binary with include_bytes!. parley shapes the runs; swash rasterizes each glyph outline into an anti-aliased coverage bitmap, cached by glyph, size and sub-pixel offset. Type below and zoom into the coverage.

interactive · live glyph rasterizer
Each cell is one pixel's alpha coverage — the fraction of the glyph outline that falls inside it. tiny-skia composites these bitmaps onto the framebuffer. Glyphs are cached in a 2,048-entry / 2 MB LRU.
parley · shapingswash · outline → coverageInter · Roboto Mono (embedded)
STAGE 04–05 / REPLACED CONTENT + LAYOUT

Boxes find their place.

Images (PNG/JPEG via zune-jpeg) and <video> contribute intrinsic sizes, then taffy solves the box tree with block, flexbox and grid. Drive the same knobs taffy exposes and watch the boxes resolve live.

interactive · taffy layout playground
taffy · flex · grid · blockzune-jpeg · image decodeLayoutResult · x·y·w·h per node
STAGE 06 / SCRIPT

Author JS runs in a cage.

The page's <script> runs in QuickJS against a narrow DOM shim. A CPU-budget interrupt handler fires every few hundred operations — so a hostile loop can't hang the tab. Try to break it.

interactive · CPU budget sandbox
interrupt checks remaining: 500 / 500
🧠 JS heap capped at 8 MiB
📚 Call stack capped at 256 KiB
⏱️ Interrupt every 500 checks
🚫 No network · no host eval
rquickjs · QuickJS engineInterruptBudget · runaway-loop guard
STAGE 07 / PAINT

A display list, replayed onto pixels.

render() walks the tree into a flat Vec<PaintCommand>, then replays it onto a tiny-skia Pixmap — back to front, painter's algorithm. Step through the commands and watch a post assemble.

interactive · step the display list
tiny-skia · CPU rasterizerPaintCommand · the display list
Painting smart, not hard

It doesn't repaint the world.

When only a few elements change, Prism computes their damage rectangle, clips to it, and repaints just that region over the previous frame — reusing the rest. Click tiles to "change" them; if damage tops half the viewport it bails to a full repaint.

interactive · dirty-region repaint
cached: 40 tiles · repainted this frame: 0 · saved: 100%
Closing the loop

Input is hit-tested in priority order.

A pointer arrives as bare coordinates. Prism checks targets in a fixed order and dispatches into the sandbox; anything it can't do itself leaves as a queued action. Click a region of the post to trace the cascade.

interactive · click a region of the post
▶ media control over <video controls>
🎚 slider input[type=range]
🔘 button <button>
🔗 link <a href>
¶ text / div generic target
Click a region to see which check catches it.
The deep end

A whole video player, from scratch.

No browser <video>, no MSE, no WebCodecs, no ffmpeg. Prism demuxes WebM, decodes VP8 to RGBA and Vorbis to PCM, all in WASM. Drag the playhead: seeks snap back to the nearest ◆ keyframe and decode forward. Click the byte bar to stream a range.

interactive · keyframe seek + byte-range streaming
video frames · 30 fpst = 0.00s
bytes loaded · click gaps to fetch a 206 range0 / 323 KB
Drag the white playhead to seek.
matroska-demuxeroxideav-vp8 · I420→RGBAsymphonia · Vorbis→PCM
Every animation frame

The runtime heartbeat.

By the numbers

One module, a lot of engine.

~0k
LINES OF RUST
1.7 MB
WASM OVER THE WIRE (6.2 MB RAW)
0
SETUP STAGES
0
BROWSER VIDEO / DOM APIs
0+
CORE RUST CRATES
2–5 ms
PER VP8 FRAME @ 480×270
0
JS OPS PER INTERRUPT CHECK
RGBA
THE ONLY THING THAT EXITS
⚗ Experimental · for fun

An experimental prototype, built for fun.

Prism is a for-fun side project, not a product. It implements a growing subset of HTML, CSS and JavaScript — there is no full CSS coverage and plenty is unsupported or rough around the edges. It is also not open source (yet).

If any of this is interesting — questions, ideas, or you'd like to collaborate — get in touch: