
I built a browser that doesn’t understand HTML. Here’s why. I’ve spent over a decade...

I’ve spent over a decade building for the web. JavaScript, React, Node.js — the full stack. I’ve shipped products that served millions. I’ve watched the web evolve from jQuery spaghetti to TypeScript monorepos with 400MB node_modules directories.
And somewhere along the way, I started asking a question that felt almost heretical:
So I built Oxide — a browser that fetches and runs WebAssembly binaries instead of HTML documents. No DOM. No CSS parser. No JavaScript engine. Just compiled code, a canvas, and a capability-based sandbox.
This is the story of why I built it, what I learned, and how you can build apps for it today.
HTML was invented in 1993 to share physics papers. Thirty-three years later, we use the same document markup language to build Figma, Google Sheets, and real-time multiplayer games.
Think about that.
Every time you open a modern web app, your browser:
The browser is the most sophisticated piece of software most people use daily. And most of its complexity exists to work around decisions made before the word “app” meant anything digital.
Not “let’s fix the web.” Not “let’s add another spec to the pile.” What if we asked: if you were designing an application delivery platform today, with no backwards compatibility constraints, what would it look like?
Here’s my answer:
That’s Oxide.
Here’s the thing people miss about WebAssembly: it’s not just “fast JavaScript.” It’s a fundamentally different delivery model.
When a traditional browser loads a page, it downloads text, parses it into trees, resolves dependencies, compiles code on the fly, and eventually — maybe — starts rendering. Each step is a potential failure point. Each step adds latency.
When Oxide loads an app, it downloads a .wasm binary and instantiates it. That’s it. The binary is already compiled. There’s no parsing phase. No tree construction. No style resolution. No layout pass. The app calls drawing functions and pixels appear.
Traditional browser pipeline: DNS → TCP → TLS → HTTP → HTML parse → CSS parse → JS parse → JS compile → JS execute → DOM build → Style resolve → Layout → Paint → Composite
Oxide pipeline: DNS → TCP → TLS → HTTP → WASM instantiate → Execute → Paint
Every step you remove is latency you eliminate and complexity you don’t have to debug.
The web’s security model is fundamentally broken. CORS, CSP, SOP, XSS, CSRF — we have more acronyms for security problems than most fields have for their entire domain.
Why? Because the browser gives applications enormous implicit capabilities and then tries to claw them back with policies. A web page can, by default, make network requests, access cookies, run arbitrary code, and interact with dozens of browser APIs. Security is a game of whack-a-mole: deny this, restrict that, sanitize the other thing.
Oxide inverts this. A guest module starts with zero capabilities:
Every capability — drawing to the canvas, making an HTTP request, reading the clipboard — is an explicit host function that the runtime provides. The guest can only do what the host allows. There’s nothing to claw back because nothing was given by default.
This isn’t theoretical. It’s how operating systems have worked for decades. It’s how mobile apps work. The web is the outlier.
Enough philosophy. Let’s build something.
Oxide apps are Rust libraries compiled to wasm32-unknown-unknown. The SDK provides safe wrappers around host capabilities. Here’s the full workflow:
rustup target add wasm32-unknown-unknown
cargo new --lib my-app
cd my-app
[package]
name = "my-app"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
oxide-sdk = { path = "../oxide/oxide-sdk" }
The cdylib crate type is critical — it tells the compiler to produce a standalone .wasm module.
use oxide_sdk::*;
#[no_mangle]
pub extern "C" fn start_app() {
canvas_clear(22, 22, 35, 255);
canvas_text(40.0, 50.0, 32.0, 255, 255, 255, "Welcome to Oxide");
canvas_text(40.0, 90.0, 18.0, 160, 160, 180,
"A binary-first browser for the post-HTML web");
canvas_rect(40.0, 130.0, 300.0, 4.0, 100, 80, 220, 255);
canvas_rect(40.0, 160.0, 160.0, 80.0, 45, 45, 70, 255);
canvas_text(55.0, 195.0, 14.0, 180, 180, 200, "No DOM");
canvas_rect(220.0, 160.0, 160.0, 80.0, 45, 45, 70, 255);
canvas_text(235.0, 195.0, 14.0, 180, 180, 200, "No CSS");
canvas_rect(400.0, 160.0, 160.0, 80.0, 45, 45, 70, 255);
canvas_text(415.0, 195.0, 14.0, 180, 180, 200, "No JS Engine");
log("App rendered successfully.");
}
cargo build --target wasm32-unknown-unknown --release
Open Oxide, click Open File, select target/wasm32-unknown-unknown/release/my_app.wasm.
Done. Your app is running in a sandboxed binary runtime with zero web tooling.
No webpack. No babel. No npm. No package.json. No polyfills. One command. One binary.
Interactive Apps with on_frame Static rendering is just the start. Export on_frame and Oxide calls it every frame, giving you an immediate-mode UI loop:
use oxide_sdk::*;
static mut COUNTER: i64 = 0;
#[no_mangle]
pub extern "C" fn start_app() {
log("Interactive app loaded.");
}
#[no_mangle]
pub extern "C" fn on_frame(_dt_ms: u32) {
canvas_clear(22, 22, 35, 255);
let count = unsafe { COUNTER };
canvas_text(40.0, 50.0, 28.0, 255, 255, 255,
&format!("Count: {}", count));
if ui_button(1, 40.0, 80.0, 100.0, 32.0, "+1") {
unsafe { COUNTER += 1; }
}
if ui_button(2, 160.0, 80.0, 100.0, 32.0, "Reset") {
unsafe { COUNTER = 0; }
}
}
Buttons, checkboxes, sliders, text inputs — all available as immediate-mode widgets. No component lifecycle. No state management library. No re-render diffing. You draw your UI every frame and the runtime handles the rest.
Protobuf as the Native Wire Format The web chose JSON as its lingua franca. JSON is human-readable, schema-less, and roughly 2–10x larger than equivalent binary representations. Every field name is repeated in every message. Every number is a string. Parsing it requires allocating strings and building hash maps.
Oxide ships with a built-in protobuf encoder/decoder — zero dependencies, wire-compatible with every protobuf implementation on Earth:
use oxide_sdk::proto::{ProtoEncoder, ProtoDecoder};
let msg = ProtoEncoder::new()
.string(1, "nikhil")
.uint64(2, 31)
.bool(3, true);
let resp = fetch_post_proto("https://api.example.com/users", &msg)
.unwrap();
let mut dec = ProtoDecoder::new(&resp.body);
while let Some(field) = dec.next() {
match field.number {
1 => log(&format!("id: {}", field.as_u64())),
2 => log(&format!("status: {}", field.as_str())),
_ => {}
}
}
Typed fields. Compact encoding. No key strings repeated per message. This is what network communication should look like between machines.
One .wasm module can load another at runtime:
let result = load_module("https://cdn.example.com/widget.wasm");
The child module shares the canvas and console but gets its own isolated memory and fuel budget. Think script tag but with real isolation. A malicious third-party widget can’t read your app’s memory. It can’t exhaust your fuel budget. The sandbox applies at every level.
What if browsers had been binary-first from the start?
We wouldn’t have CSS specificity. We wouldn’t have !important. We wouldn’t need virtual DOMs because there would be no DOM. We wouldn’t have 2,000-line webpack configs. We wouldn’t need TypeScript because apps would be written in typed languages from day one. Front-end engineering would be indistinguishable from software engineering.
What if security was additive, not subtractive?
Instead of giving apps everything and trying to lock it down (and constantly failing — XSS is a 25-year-old bug class that still tops the OWASP list), what if apps started with nothing and explicitly requested capabilities? Mobile platforms proved this model works. Oxide applies it to the browser.
What if the browser was just a runtime?
Strip away the HTML parser, the CSS engine, the JavaScript JIT, the DOM, the layout engine. What’s left? A sandboxed execution environment that can render graphics and make network requests. That’s Oxide. That’s also, arguably, what the browser has been trying to become for two decades.
What if we measured progress by what we removed?
The tech industry celebrates addition. New frameworks. New build tools. New layers of abstraction. But the most impactful innovations often come from subtraction. Unix pipes. REST. WebAssembly itself. Oxide is an experiment in subtraction: what if we removed everything the modern web added to compensate for HTML’s limitations, and started from the capability that web apps actually need?
Let me be direct about what this is and isn’t.
Oxide is not a production browser. It’s not going to replace Chrome. It’s a proof-of-concept — a working argument for what the next era of application delivery could look like.
It doesn’t have accessibility APIs (yet). It doesn’t have text selection or find-in-page. It doesn’t have a robust layout engine because it deliberately doesn’t have a layout engine — that’s the point.
But every technology that changed the world started as a toy that “real engineers” dismissed. Unix was a toy. The web was a toy. WebAssembly was an experiment.
Here’s what keeps me up at night:
We’ve spent 30 years optimizing a document viewer to behave like an application runtime. What if we’d spent those 30 years building an application runtime?
Oxide is my attempt at answering that question. It’s written in Rust, it’s open source, and it works today. Clone it, build an app, break it, tell me what’s wrong with it.
The best ideas survive contact with reality. Let’s find out if this one does.
Oxide is open source at:
https://github.com/niklabh/oxide https://oxide.foundation/
Nikhil Ranjan is a senior software engineer based in Berlin. He builds scalable systems and publishes open source software. Find him on GitHub and X.
aiMost of us have seen a coding agent fail to complete a task we know it can do. We just don't...
googlecloudWhen building Generative AI applications, developers often encounter a massive bottleneck: sequential...
discussI’ve been thinking about sharing some electronic circuit posts on Dev.to — small circuits, DIY...
agentsWhat nobody tells you about exporting your multi-agent prototype to a local workspace. Every...
agenticarchitectAutonomous agents are genuinely good at answering messy business questions. Give one an LLM and a set...
aiPR volume went up, ticket quality didn't, and the gap got filled with LLMs on both sides of the review: bots reviewing, bots replying, bots occasionally arguing with bots about priorities that only existed in a teammate's head. Our CEO named the actual problem, and it's bigger than code review.