Skip to content

Fidelity

Fidelity

An honest account of what the renderer matches in a browser and what it does not. The authoritative, per-page and per-phase assessment (five live pages, committed golden PNGs, before/after deltas) lives in the engine’s FIDELITY.md, and the measured-vs-Chrome numbers in bench/REPORT.md.

Measured vs headless Chrome

Windowed SSIM (1.0 = identical) over the common top-left region at 1024px width; speed× = chrome_ms / webengine_ms (>1 = webengine faster). Timings include the live network fetch, so they vary run to run.

URLSSIMpixdiff %speed×note
example.com/0.8315.618.3near-parity; its JS cross-fade is not modelled
en.wikipedia.org/wiki/Go0.41730.31.6live article; JS and rate-limited thumbnails
pkg.go.dev/net/http0.71611.30.8large computed page
go.dev/blog/0.70016.60.45slower than Chrome
react.dev/0.72533.01.3SPA; hydration fails, so the static fallback renders
news.ycombinator.com/0.61513.71.75table layout; rotating front page
developer.mozilla.org/…/CSS0.60618.13.3docs layout
github.com/golang/go0.63714.22.0live repository counters
tailwindcss.com/0.72412.20.5sponsor carousel rotates; slower
caniuse.com/0.66018.00.8data grid

Mean SSIM over the ten pages is ≈ 0.66. Summed over the run, webengine took 26.1 s and headless Chrome 26.3 s, so overall time is at parity, but the per-page spread is wide. The Wikipedia number is confounded by runtime JS chrome and by rate-limited image thumbnails.

Works today

  • Full box-model layout: block/inline flow, floats + clear, flexbox, CSS grid, tables (with vertical-align on cells; a display:block table keeps its grid, CSS 2.1 §17.2.1, engine v0.45.10; width: min-content | max-content | fit-content), position (relative/absolute/fixed/sticky), margin collapsing, greedy word-wrap, multi-column layout.
  • Effects and lists: translate/rotate, filter, backdrop-filter, mask-image (a single url() mask). ul/ol list markers are generated in layout as a ::marker box, so every backend gets them as text (the raster canvas, the vector PDF, go-pdfkit/html2pdf): list-style-type disc, circle, square, decimal, decimal-leading-zero, lower-/upper-alpha, lower-/upper-roman, a <string> and none; list-style-position outside/inside; ::marker colour, font and content; HTML start, reversed, value and type (engine v0.18.0).
  • Generated content and counters: ::before/::after with strings, counter() and counters(); counter-reset (with reversed()), counter-increment, counter-set, scoped and inherited per CSS Lists 3, and the implicit list-item counter. CSS escapes ("\2022\00a0", .\31 0) are decoded in every string, identifier and selector.
  • CSS: cascade + specificity (inline > id > class > tag) + inheritance; var() custom properties; @media width queries; dark-mode (prefers-color-scheme); external <link> stylesheets; UA defaults.
  • Selectors: tag/class/id/compound, descendant + child + sibling (~/+) combinators, :checked, :not() (the checkbox-hack that collapses MediaWiki dropdowns); unmodelled selectors reduce rather than drop the rule.
  • Colour & decoration: named/#rgb/#rrggbb, modern rgb()/hsl(), background-color, linear/radial gradients (with background-size, background-position and background-repeat, engine v0.45.10), background-image: url(), border + border-radius, box-shadow, group opacity.
  • Text: anti-aliased proportional text with real bold and italic faces (no faux-bold); serif / sans / mono; complex scripts (Cyrillic, Vietnamese, …); white-space: pre; the raster painter draws text at the exact size with a fractional pen and quarter-pixel glyph origins, so word positions agree with Chrome to within half a pixel (v0.45.12); the generic monospace has Go Mono bold, italic and bold italic (v0.45.12); variable fonts drawn at each font-weight through their wght axis (v0.45.10; the bundled Inter and Lora regulars too, so 300 and 500 are their own weights); the font shorthand (v0.45.11). The render CLI with -file uses the file’s directory as its base, so a relative @font-face font beside it loads.
  • Images: <img> over http(s) + data: (PNG/JPEG) and SVG (oksvg/rasterx) via <img *.svg>, data:image/svg+xml and inline <svg>.
  • JavaScript: page scripts run via goja against a real DOM, with fetch()/XHR and read-back of real laid-out geometry (getBoundingClientRect, offset*, getComputedStyle). A settle-then-render loop re-cascades and re-lays-out after scripts mutate the DOM (including dynamically injected <script>/<style>/<link>), to a bounded fixpoint. The same JS-settled DOM drives the click hit-map.

Not supported yet (stated, not hidden)

  • conic-gradient is recognised but not painted. filter and mask-image are modelled narrowly: a mask is a single url() stretched over the box. scale and skew are not supported. SVG has no <filter>/<mask>/<pattern>/ <text>/embedded <image>, and a per-page image budget caps very icon-heavy pages.
  • ::before/::after take strings, counter() and counters(); attr(), url() and the quote keywords are not supported, nor is @counter-style or list-style-image. A form control’s own ::before/::after is not drawn.
  • Rate-limited image hosts (Wikimedia) answer some requests with HTTP 429. The engine retries them and the renders are identical, but each retry adds about two seconds to that page.
  • Large computed pages (pkg.go.dev, go.dev) render slower than Chrome — a perf gap, not a fidelity one.
  • This is not a standards-complete browser and is not claimed to match Chromium pixel-for-pixel. It is an honest renderer whose gap to a browser is the roadmap, measured page by page rather than asserted.

How to read a render

example.com renders at near-parity. A JS-driven SPA (react.dev) renders its gradients, SVG atoms and script-built content and lands around 0.73 SSIM. A large computed docs page (pkg.go.dev) renders correctly but slower than Chrome. A modern CMS page (Wikipedia) collapses its Vector-2022 chrome via the checkbox-hack and mw.loader, but the SSIM stays noisy where runtime chrome and icon fonts differ — the residual is documented, not hidden. The gap between these is exactly the remaining-levers section of the roadmap.