Home
go-webengine
A pure-Go, CGO_ENABLED=0 headless web engine. Give it a URL, get back an
image of the page — HTML/CSS layout and paint, with JavaScript executed
against a real DOM, and no Chromium, no cgo and no host web view.
It is the rendering core of the
browserproxy remote-browser
service and the wasmdesk in-desktop browser: a
server renders pages and streams frames (plus a click hit-map) to a thin client.
What it renders today
- Full box-model layout: block, inline, floats + clear, flexbox, CSS
grid, tables,
position(relative/absolute/fixed/sticky), margin collapsing, word-wrap. - Broad CSS: cascade + specificity + inheritance,
var()custom properties,@mediawidth queries, dark-mode, external<link>stylesheets, modernrgb()/hsl(), linear/radial gradients,background-image, border + border-radius, box-shadow, opacity; sibling combinators,:checked,:not(). - Effects and layout:
translate/rotate,filter,mask-image, list markers (list-style-type,::marker,inside/outside), CSS counters (counter()/counters()in::before/::after), multi-column layout,vertical-alignon table cells. - Text: anti-aliased serif/sans/mono with real bold and italic faces.
- Images: PNG/JPEG,
data:, and SVG (<img>,data:and inline<svg>). - JavaScript: page scripts run via goja
against a real DOM with
fetch()/XHR and laid-out-geometry read-back; a settle-then-render loop reflects script-driven DOM mutations in the output.
See Fidelity for the honest, measured account of what matches a browser and what does not.
Pipeline
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)Quickstart
import (
"context"
"image"
"os"
"github.com/go-webengine/engine"
)
png, err := engine.Screenshot(
context.Background(),
"https://example.com/",
image.Rect(0, 0, 1024, 768),
)
if err != nil {
panic(err)
}
_ = os.WriteFile("out.png", png, 0o644)engine.Render returns the raw *image.RGBA plus a RenderInfo (Title,
URL, ContentHeight). For more control, engine.New() gives an *Engine with
RenderHTML (offline), RenderWithLinks (image + click hit-map) and a
DisableJS field. The viewport width is fixed; the height grows to fit the whole
page. A render CLI is bundled:
go run github.com/go-webengine/engine/cmd/render \
-url https://example.com/ -out out.png -w 1024 -h 768Security
The engine fetches every resource a page names: images, stylesheets, fonts,
scripts and modules, fetch/XHR targets and form posts. It does not filter
destinations itself, so rendering an untrusted page from a machine with access to
private networks can reach internal addresses. Embedding the engine directly means
adding a dial-time guard through Engine.Client that refuses loopback, private and
link-local addresses; the browserproxy
service ships one.
Since a renderer’s whole job is turning arbitrary, untrusted page content into output, several unrelated kinds of pathological input have been found and capped, each confirmed live against a synthetic reproduction:
- Decoding: raster images declaring more than 25 megapixels are refused before decode; every fetch is size-capped.
- Tree-depth recursion: a script-built DOM tree past 512 levels used to
crash the whole process with an unrecoverable stack overflow in the CSS
cascade, layout,
cloneNode/isEqualNode, and three CSS parsers (calc(),:not()/:has()/:host(),:is()/:where()/:matches()). All are now capped at 512 or a generous output budget. - Memory exhaustion: a single
innerHTMLassignment, or acloneNodecall repeated in a loop, could previously allocate tens of gigabytes of real memory from a few lines of ordinary-looking script.cloneNodeand the HTML→DOM conversion shared byParse/ParseFragment/innerHTMLnow cap total node count per call; the rendered canvas is capped by pixel count. - Unresponsive render on ordinary content: a real, non-malicious page
with several
background-repeatsections could take over a minute to paint — each stamped pixel went through an interface call that both allocated and re-derived colour math that only needed doing once per source tile, not once per painted pixel. Fixed by converting the tile to a fast, direct-access format once, before the repeat loop runs.
See the engine’s FIDELITY.md
for the full measured writeups. None of this changes how a well-formed page
renders — only how a pathological one degrades (truncated output, never a
crash or unbounded allocation).
Where to next
- Architecture — the packages, the reuse-vs-build decision, and why prior pure-Go browsers were studied but not built on.
- Roadmap — the phases from static render to the shipping remote-browser proxy, and the remaining levers.
- Fidelity — an honest, measured account of what renders like a browser today and what does not.
Links
- Source: https://github.com/go-webengine/engine
- Remote-browser service: https://github.com/go-webengine/browserproxy
- Landing: https://go-webengine.github.io/
- License: BSD-3-Clause