Files
obscura/RENDER.md
SGavrl 48e3fac55a docs: add RENDER.md with usage and benchmark
Documents the obscura-render layer: how to enable it (--features render),
the CLI --screenshot and CDP Page.captureScreenshot paths, what it
renders, the architecture, and a head-to-head benchmark.

Benchmark vs headless Chrome (same page, 1280x720, warmed):
- local fixture: obscura ~0.06s/33MB vs chrome ~0.90s/195MB
- example.com:  obscura ~0.10s/34MB vs chrome ~0.98s/199MB
Roughly 10-15x faster and ~6x less memory. Chrome's larger PNG on the
real page reflects that it paints text, which obscura does not yet.
2026-07-01 21:37:38 +00:00

3.6 KiB

obscura-render

An optional, scoped render layer for Obscura. It adds real CSS box geometry to the JS surface and rasterizes PNG screenshots, pure Rust, behind an opt-in feature. The default scraping build is unchanged.

Why

Obscura's speed and low memory come from not carrying a full browser rendering pipeline. This layer adds only what scraping and automation need, real box geometry (so getBoundingClientRect, elementFromPoint, and IntersectionObserver return true values) and screenshots, without the weight of a Chromium-class engine.

Enable

cargo build --release --features render

The feature propagates from obscura-cli through obscura-browser, obscura-cdp, and obscura-js to obscura-render. The default build compiles it out entirely.

Use

CLI screenshot of the settled page:

obscura fetch --screenshot out.png https://example.com/

CDP (Puppeteer / Playwright page.screenshot()), driving obscura serve:

await page.goto('https://example.com/');
await page.screenshot({ path: 'out.png' });   // Page.captureScreenshot

Real geometry in JS (no screenshot needed):

const r = await page.evaluate(() => document.querySelector('div').getBoundingClientRect());
// r.x, r.y, r.width, r.height are laid-out values, not synthetic

What it renders

  • Block, flexbox, and CSS grid box layout via taffy.
  • Inline styles plus a small built-in UA default sheet, parsed for the layout-relevant properties (display, width/height, margin, padding, border) and background-color.
  • Background colors painted to the pixmap (backgrounds, borders, and text are staged below).

Benchmark

Same page, same 1280x720 viewport, warmed, on the same host. Headless Chrome is google-chrome --headless=new --screenshot.

page engine wall peak RSS PNG
local fixture obscura ~0.06s ~33 MB 5.3 KB
local fixture chrome ~0.90s ~195 MB 4.4 KB
example.com obscura ~0.10s ~34 MB 5.2 KB
example.com chrome ~0.98s ~199 MB 17 KB

Obscura is roughly 10-15x faster and uses about 6x less memory than headless Chrome for a screenshot. Chrome's larger PNG on example.com is because it renders text, which obscura does not yet paint.

Limitations and next steps

These are tracked enhancements, not bugs:

  • Text is not rendered yet (the biggest fidelity gap).
  • Borders and images are not painted; only background colors.
  • CSS coverage is the layout-relevant subset. Full cascade (selector matching, media queries, inheritance of all properties, relative units) is not implemented.
  • The layout cache is computed lazily and cleared on navigation. It is not invalidated on every DOM mutation, so geometry read mid-script may lag a frame; reading after settle is reliable.

Architecture

obscura-render is its own workspace crate with two optional capabilities:

  • Layout (default): layout_dom(dom, viewport) walks a DomTree, computes each element's style, builds a taffy tree, and returns border-box geometry keyed by NodeId. Exposed to JS by op_layout_geometry (feature-gated).
  • Paint (paint feature): paint_dom / screenshot_png rasterize the laid-out tree with tiny-skia (CPU, pure Rust, deterministic) to a pixmap or PNG bytes.

The JS surface stays synthetic when the feature is off: bootstrap.js probes op_layout_geometry with typeof and falls through to the existing synthetic rect, so the default build is byte-for-byte equivalent.