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

99 lines
3.6 KiB
Markdown

# 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
```bash
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:
```bash
obscura fetch --screenshot out.png https://example.com/
```
CDP (Puppeteer / Playwright `page.screenshot()`), driving `obscura serve`:
```js
await page.goto('https://example.com/');
await page.screenshot({ path: 'out.png' }); // Page.captureScreenshot
```
Real geometry in JS (no screenshot needed):
```js
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](https://github.com/dioxusLabs/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](https://crates.io/crates/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.