Modern "render a web page" tooling almost always means shipping a whole
browser — Chromium via CDP, a WebView, cgo bindings. go-webengine renders in
pure Go. It fetches the page, builds an owned DOM over
x/net/html, runs a real CSS cascade
(specificity + inheritance, var(), @media, dark-mode), executes the
page's JavaScript (goja) against a real DOM, lays
out a full box model (block, inline, float, flex, grid, table, position), and
paints anti-aliased text (go-opentype),
gradients, box-shadows, images and SVG onto an image.RGBA. One static binary, no
C toolchain, identical on every OS.
Fetch (go-browserhttp) → Parse (x/net/html → dom) → Cascade (css: +var()/@media/dark-mode)
→ JavaScript (js: goja + real DOM + fetch/XHR)
→ Layout: block · inline · float · flex · grid · table · position (layout)
↻ settle loop: JS reads laid-out metrics → re-cascade + re-layout to a bounded fixpoint
→ Paint: AA text/SVG/gradients/shadows + backgrounds (go-widgets/painter) + images (go-images)
→ image.RGBA → PNG (+ optional link hit-map)It is the rendering core of the
browserproxy
remote-browser service and the wasmdesk in-desktop
browser. It is not a Chromium replacement and not claimed to match it
pixel-for-pixel: example.com renders at near-parity (0.831 SSIM, ~18×
faster on the latest run), JS-heavy and large computed pages are the honest frontier (mean SSIM ≈ 0.66).
What works and what does not is documented honestly, page by page and measured against
headless Chrome, in the engine's
fidelity report
and benchmark.
Repositories
The rendering core
Fetch a URL โ DOM โ CSS cascade (var()/@media/dark-mode) โ run the page's JavaScript โ full box-model layout (flex/grid/tables/position) โ paint anti-aliased text, gradients, images and SVG to an image.RGBA / PNG. Pure Go, CGO_ENABLED=0, no Chromium; ships a `render` CLI.
One function, against one leak
A safe <code>ResolveDir</code> for esbuild's <code>pkg/api</code>. A caller bundling untrusted source installs an <code>OnResolve</code>/<code>OnLoad</code> plugin and it looks complete โ every import in its own tests goes through it. But the bundler expands a <strong>glob dynamic import</strong>, <code>import(`./${lang}/index.js`)</code>, by walking <code>ResolveDir</code> on the <strong>real filesystem</strong> through its internal resolver, bypassing the plugin entirely. Whatever that directory is โ <code>"/"</code> is a common default โ is what gets walked, following symlinks. This returns a directory where that walk finds nothing.
Remote-browser service
Renders pages server-side with the engine and streams frames (plus a click hit-map) to a thin client over a JSON WebSocket protocol, forwarding clicks/scrolls/keys back as navigation. SSRF-guarded. The server half of the wasmdesk in-desktop browser.
Browser-automation CLI
An HCL-driven browser-automation CLI on the engine: test scenarios that click, fill forms and assert, with no Chromium.
Roadmap
Phase 0โ1 · Static render + box modelshipping
HTML parse, a real CSS cascade (specificity + inheritance), and a full box model โ block/inline flow, floats, flexbox, CSS grid, tables and position โ painted to an image.
Phase 1.9โ2.1 · CSS breadth + SVGshipping
var() custom properties, @media, dark-mode, modern colour, gradients (sized and tiled with background-size), variable-font weights, the font shorthand, sub-pixel text positioning, bold and italic monospace, background-image, border-radius, box-shadow, opacity, and pure-Go SVG rasterisation.
Phase 2โ2.4 · JavaScript + dynamic rendershipping
goja bound to a real DOM with fetch()/XHR and laid-out-geometry read-back; a settle-then-render loop reflects script-driven DOM mutations, on both the plain and click-hit-map paths.
Phase 3 · browserproxyshipping
A WebSocket remote-browser service (its own repo) renders server-side with the engine and streams frames to the wasmdesk clients/browser front-end.
Phase next · Remaining leversplanned
Closing fidelity residuals (icon fonts, the rest of the content grammar, conic-gradient/filter/mask, SVG filters) and the perf gap on large computed pages. List markers, ::marker and CSS counters shipped in v0.18.0. Diminishing returns โ mean SSIM โ 0.69, example.com at 0.954.
Pure Go, CGO_ENABLED=0, cross-built for all six of Go's
64-bit targets. The pure-logic packages (cascade, line-breaker, box metrics, DOM) are
asserted at exact geometry behind a ratchet coverage gate; a committed golden PNG covers the
offline paint path. Prior pure-Go browser work (opossum / mycel) was studied but not built
on — its layout core is stub-quality; see the survey.