Obscurax

CI codecov Hex.pm Elixir License: MIT

Elixir binding for the obscura headless browser engine, built with Rustler. The engine is pinned to obscura v0.2.4 (deno_core 0.412, V8 150.4.0).

Obscurax drives a real JavaScript engine from Elixir — but not the way a WebDriver binding does. There is no Chrome to install, no driver process to supervise, and no Node or Python in the loop: the engine is a Rust shared library shipped as a precompiled NIF, and every page gets its own OS thread and V8 isolate so pages execute JavaScript genuinely in parallel.

At a glance

Engine obscura v0.2.4 — Rust headless browser with V8 150.4.0 embedded
Runtime dependencies rustler_precompiled (that's it)
External browser or driver none
Elixir / OTP 1.18+ / OTP 26+
Precompiled targets 4 (macOS arm64, Linux arm64, Linux x86-64, Windows x86-64)
Parallelism model one OS thread + one V8 isolate per page

Why obscurax

Performance and footprint

The engine runs in-process: there is no browser to launch, no driver in between, and no IPC between your Elixir code and V8. obscura benchmarks itself against headless Chrome, and obscurax embeds that same engine:

Metric obscura Headless Chrome
Memory 30 MB 200+ MB
Binary size ~70 MiB 300+ MB
Page load 85 ms ~500 ms
Startup Instant ~2 s

Page load by page type:

Page obscura Chrome
Static HTML 51 ms ~500 ms
JS + XHR + fetch 84 ms ~800 ms
Dynamic scripts 78 ms ~700 ms

Embedding the engine rather than driving a browser has two consequences worth knowing:

These are obscura's published figures, from the suite at h4ckf0r0day/obscura-benchmark — WPT conformance, an obstacle course, a real-world corpus, and vs-Chrome speed. They were not re-measured for obscurax.

Installation

Add obscurax to your mix.exs:

def deps do
  [
    {:obscurax, "~> 0.2.0"}
  ]
end

Build requirements

By default, Obscurax downloads a precompiled NIF at compile time — no Rust toolchain required. The precompiled binaries are hosted on GitHub Releases and verified with SHA-256 checksums.

Precompiled target Platform
aarch64-apple-darwin macOS on Apple silicon
aarch64-unknown-linux-gnu Linux on arm64
x86_64-unknown-linux-gnu Linux on x86-64
x86_64-pc-windows-msvc Windows on x86-64

To force building from source instead (e.g. for development or unsupported targets), set OBSCURAX_BUILD=true:

OBSCURAX_BUILD=true mix compile

The first source build takes ~5 minutes because V8 builds from source. Subsequent builds are fast — the V8 artifact is cached by Cargo.

Rustler compiles the Rust crate into a native shared library (obscurax.so on Linux/macOS, obscurax.dll on Windows) and places it in priv/native/.

Quick start

{:ok, browser} = Obscurax.Browser.new()
{:ok, page} = Obscurax.Browser.new_page(browser)
:ok = Obscurax.Page.goto(page, "https://example.com")
{:ok, title} = Obscurax.Page.evaluate(page, "document.title")
IO.puts(title)
Obscurax.Page.close(page)

Bang variants raise Obscurax.Error on failure:

browser = Obscurax.Browser.new!()
page = Obscurax.Browser.new_page!(browser)
Obscurax.Page.goto!(page, "https://example.com")
"Example Domain" = Obscurax.Page.evaluate!(page, "document.title")

Mock a network response without leaving Elixir:

:ok = Obscurax.Page.enable_interception(page)

receive do
  {:obscurax_intercept, ref, %{url: url}} ->
    :ok = Obscurax.reply_intercept(page, ref, {:fulfill, 200, %{}, ~s({"ok":true})})
end

Features

Browser and pages

Navigation and JavaScript

Elements

Network observability

Cookies

Concurrency

Pages from the same browser run in true parallel — V8 evaluation in one page never blocks another:

{:ok, browser} = Obscurax.Browser.new()

urls = ["https://example.com", "https://example.org", "https://example.net"]

tasks =
  for url <- urls do
    Task.async(fn ->
      {:ok, page} = Obscurax.Browser.new_page(browser)
      :ok = Obscurax.Page.goto(page, url)
      {:ok, html} = Obscurax.Page.content(page)
      Obscurax.Page.close(page)
      html
    end)
  end

results = Task.await_many(tasks, 30_000)

How it works

FAQ

Does Obscurax need Chrome, Chromium, or a WebDriver? No. It binds directly to obscura, a browser engine written in Rust with V8 embedded. There is no Chrome download, no chromedriver, no geckodriver, and no Node or Python.

Do I need Rust installed to use it? No. rustler_precompiled downloads a SHA-256-verified binary for your platform at compile time. Rust 1.91+ and a C++ compiler are only needed with OBSCURAX_BUILD=true.

Can multiple pages really run JavaScript at the same time? Yes. Each page has its own OS thread and its own V8 isolate, and JavaScript evaluation runs on dirty CPU schedulers, so pages do not queue behind each other or behind the BEAM.

Does JavaScript evaluation block my GenServers? No. Evaluation runs on dirty CPU schedulers, and each page owns a separate runtime, so unrelated BEAM processes keep running.

How do I mock or block a request? Call Obscurax.Page.enable_interception/1, receive {:obscurax_intercept, ref, req}, then call Obscurax.reply_intercept/3 with :continue, {:fulfill, status, headers, body}, or {:fail, reason}.

How are errors reported? Non-bang functions return {:error, %Obscurax.Error{}} with a kind, a message, and a context map; bang variants raise the same struct.

Can I navigate to local HTML files? Yes, with Obscurax.Browser.new(allow_file_access: true). The gate is off by default because a file:// URL is a local filesystem read; a page can never navigate itself from a web origin into file:// regardless of this option.

What do I need to change if I upgrade? See CHANGELOG.md. 0.2.0 moved file:// navigation behind the :allow_file_access opt-in and fixed Browser.new/1 options that were previously ignored.

Current limitations

License

MIT