A Headless Browser Without a Rendering Engine, Built for Agents
Most developers have tried a headless browser like Puppeteer or Playwright at least once. AI agents use them too, for tasks like crawling and scraping.
The issue I kept running into is that headless browsers are heavy on CPU and memory. I hit the same thing running browser tools inside coding agents: a single page load is fine, but a few in parallel and the machine starts to drag.
Recently I came across a GitHub project called Lightpanda Browser, which claims to solve exactly this problem.
Most headless browsers, including Puppeteer and Playwright, run on top of Chromium and drive it through the Chrome DevTools Protocol (CDP), which means every page load still goes through Chromium's full rendering pipeline: layout, paint, compositing, and GPU-based rasterization, even though nothing is ever displayed on a screen.
Lightpanda is built from scratch to avoid that cost.
In this article, we'll go through what Lightpanda actually does differently from Chromium.
Installation
We won't go into full detail on setting this up. I tried the binary version, using this command:
curl -L -o lightpanda https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-x86_64-linux && \
chmod a+x ./lightpanda
Other install options, including Docker, Mac, and MCP, are covered in the README's install section.
Agent Mode
Lightpanda also ships with a built-in agent mode. Instead of writing a script yourself, you run lightpanda agent and describe what you want in plain English, or use slash commands.
The Architecture Behind the Speed
Now let's get into the architecture and techniques behind why Lightpanda is faster than other headless browsers. This section splits into four parts:
- No rendering pipeline - Lightpanda skips layout, paint, and GPU compositing entirely, since none of that is needed when nothing is ever shown on a screen. This does rule out a few things, which we get to later.
- The stack underneath - the actual pieces that make up Lightpanda: Zig, V8, zigdom, html5ever, and libcurl.
- Why Zig - every other browser engine is written in C++. Lightpanda picked Zig instead, and there's a reason behind that choice.
- zigdom, a custom DOM - Lightpanda didn't start with its own DOM implementation. It began with a borrowed C library, then rewrote it from scratch in Zig.
No Rendering Pipeline
A normal browser like Chrome works by running a rendering pipeline every time a page loads: it resolves styles, calculates layout for every element, paints pixels, and composites the final frame before handing it to the GPU. That's the process that turns HTML and CSS into something you actually see on screen.
Lightpanda skips all of that. It's built for automation and JavaScript execution, not for showing a page visually.
The README says this directly:
"No graphical rendering engine" is listed as one of its core design choices, right alongside not being based on Chromium, Blink, or WebKit.
You can see this in practice with two CDP commands:
Page.captureScreenshotPage.printToPDF
Neither actually renders anything. Both just return a static placeholder instead.
Canvas and WebGL are handled the same way. APIs like <canvas>, WebGLRenderingContext, and CanvasRenderingContext2D exist, along with methods like getContext and getExtension, so scripts that call them don't crash.
But none of it does real pixel rendering. getParameter, for example, just returns an empty string no matter what you pass it.
What Lightpanda actually spends its effort on instead is building the DOM tree, running JavaScript through V8, and handling CDP commands over the websocket. That's the whole job: automation and scraping, not producing something to look at.
The Stack Underneath
Lightpanda ships as a single binary and runs as a single process.
Inside that process, a handful of pieces do the actual work, and each one was picked for a specific reason.
Zig, the language everything is written in
Zig is a low-level systems language, in the same family as C and Rust.
It has no garbage collector, which means the program controls exactly when memory is allocated and freed.
That control matters for a browser.
A page load creates thousands of small, short-lived objects: DOM nodes, parsed strings, network buffers.
Zig lets Lightpanda hand all of those to a single memory region called an arena, then free the entire region in one shot when the page closes, instead of tracking each object individually.
The project's architecture docs describe a process-wide pool of these arenas: a page borrows one when it loads and returns it when it's done.
The core is in src/browser/ and the process-wide state lives in src/App.zig in the browser repo.
V8, the JavaScript engine
Lightpanda does not write its own JavaScript engine. It embeds V8, the exact engine Chrome and Node.js use, and that is a deliberate choice: writing a JavaScript engine from scratch is a multi-year effort, and running the same engine as Chrome means React, Vue, and Angular apps behave the way they would in a real browser.
The Zig code in src/browser/js/ is just a thin wrapper around V8's C++ API, handling isolates, contexts, values, and promises.
One detail speeds up startup. Lightpanda embeds a V8 snapshot in the binary at build time, a pre-built serialized copy of V8's heap with all its built-in objects already initialized.
On start, V8 loads that snapshot instead of rebuilding everything from scratch, which removes most of the warm-up cost.
html5ever, the HTML parser
Turning raw HTML text into a DOM tree is harder than it looks.
Real-world HTML is messy, and the spec has a long list of rules for handling broken markup so every browser produces the same tree.
Instead of writing this logic themselves, Lightpanda uses html5ever, the spec-compliant HTML parser from Mozilla's Servo browser project.
It's written in Rust, so Lightpanda calls it through a C interface from src/browser/parser/ and builds the DOM from the parser's callbacks.
This is why building Lightpanda from source needs a Rust toolchain in addition to Zig.
Fetching the page and its subresources is handled by libcurl, the same library behind the curl command. It already handles HTTP, HTTPS, redirects, and cookies, so there was no reason to write a new one.
Why Zig
Chromium, WebKit, and Gecko are all C++, so picking Zig, a language that hasn't even reached version 1.0, was a real bet.
The Lightpanda team explained the decision in a blog post titled Why We Built Lightpanda in Zig.
The cofounder's summary is refreshingly blunt: "I'm not smart enough to build a big project in C++ or Rust."
Behind the joke is a serious point.
Before Lightpanda, the team worked mostly in Go, and they wanted a language that felt just as simple, but with the low-level control a browser needs.
What a browser needs from a language
Two things drove the choice.
First, the browser has to embed V8, and V8 is written in C++ with no C API.
That means whatever language Lightpanda is written in has to talk to C++ code, and doing that cleanly is not trivial.
Second, a browser creates and destroys huge amounts of short-lived memory: DOM trees, JavaScript objects, parsing buffers.
When you're loading thousands of pages, every millisecond of allocation and cleanup adds up, so the team wanted direct control over how memory gets handed out and freed.
Why not C++
C++ was the obvious option, since it powers every big browser.
But the team listed three problems with it:
- Too many ways to do the same thing. After four decades of features, C++ has multiple approaches to almost everything: template metaprogramming, several inheritance patterns, different initialization styles. They wanted one clear way.
- Memory bugs. Use-after-free errors, leaks, and dangling pointers are constant risks. Smart pointers help but add complexity and runtime cost.
- Build systems. Fighting CMake and header dependencies is a known time sink, and a small team didn't want to spend days on build configuration.
Rust was the other option, but a browser engine juggles several memory regions at once, and those patterns fight Rust's borrow checker: you either work around it at a performance cost or drop into unsafe.
What Zig gives instead
Zig ended up fitting because of four things:
- Explicit allocators. Every allocation says which allocator it uses, which makes the per-page arena pattern natural: one memory region per page load, freed all at once when the page closes.
- Comptime. Zig's compile-time code generation. Lightpanda uses it to auto-generate the glue between Zig types and JavaScript, so exposing a DOM type to V8 doesn't require hand-written binding code.
- C interop. Zig can import C headers directly, which is how libcurl and V8 (through C headers generated from Deno's rusty_v8) plug in without wrapper libraries.
- Fast compilation. A full rebuild takes under a minute, much quicker than Rust or C++.
zigdom, a Custom DOM
First, a quick refresher on what the DOM is.
When a browser loads an HTML page, it doesn't keep it as text. It parses the HTML into a tree of objects in memory, where every tag becomes a node with a parent and children.
That tree is the DOM.
It's what JavaScript actually reads and changes when a script calls document.getElementById or element.appendChild.
Every browser needs one, and Lightpanda's is called zigdom.
The story of how it got there is covered in their post Migrating our DOM to Zig.
What came before
Lightpanda didn't start with its own DOM.
It used LibDOM, an existing DOM library written in C, which gave them a mostly complete implementation with very little effort.
The setup had three layers: V8 running the JavaScript, a Zig layer in the middle, and LibDOM underneath holding the actual tree.
A call like document.getElementById('spice') went from V8 into Zig, then into LibDOM, and the result came back the same way.
That worked until they tried to support more of the real web.
LibDOM's built-in event system was awkward to extend beyond basic DOM events, and adding Custom Elements and Shadow DOM, which they were writing in Zig, meant constant back-and-forth across the language boundary.
Memory management was split across layers too, which made things like future multi-threading harder to plan.
What zigdom does differently
zigdom is a DOM written entirely in Zig, so the middle layer disappears.
V8 talks to Zig, and Zig owns the tree.
A few design choices in it are specific to Lightpanda's workload:
- One allocation per element instead of five. Creating a
<div>needs a Div, an HTMLElement, an Element, a Node, and an EventTarget object, since each one builds on the last. Rather than allocating each separately, zigdom does a single allocation for the total size and splits it up. On a page with tens of thousands of nodes, that adds up. - Rarely used properties live off the element. Things like
classList,style, anddatasetexist on every element in a normal DOM, but most scripts only touch them on a handful. zigdom stores them in a separate page-level lookup instead of on each element, which removes about six pointers from every node at the cost of a small lookup. - Events, Custom Elements, and Shadow DOM live in the same codebase. Since everything is Zig, these features no longer have to bridge into a C library.
The performance gain from the rewrite was modest, single-digit percent improvements in both memory and CPU.
The real win, according to the team, was having one cohesive codebase that's much easier to extend, and better Custom Element and Shadow DOM support came almost immediately.
Is Lightpanda the Right Choice?
It depends entirely on what your script needs from the page.
Where it fits
Lightpanda works well when the job is reading and acting on the DOM, not looking at pixels:
- Scraping and data extraction. Pulling text, links, prices, or structured data from pages, including JavaScript-heavy ones built with React or Vue.
- AI agents that browse. An agent that reads a page, decides, clicks, fills a form, and reads again. The built-in
agentandmcpmodes are built for exactly this. - High-volume crawling. Anything where you run dozens or hundreds of browser instances in parallel. Chrome's memory cost is what breaks first here, and Lightpanda's small footprint is the whole point.
- Existing Puppeteer or Playwright scripts. Since it speaks CDP, switching is usually a one-line change to the connect call, and switching back is just as easy.
Where it doesn't
The same design choice that makes it fast is what rules it out for some work:
- Screenshots and PDFs. There's no renderer, so
captureScreenshotandprintToPDFreturn placeholders. Visual regression testing, PDF generation, and any agent that decides based on what the page looks like need Chrome. - Canvas and WebGL. The APIs exist so scripts don't crash, but nothing is actually drawn. Sites that render their content on a canvas won't give you anything useful.
- Partial Web API coverage. It's still in beta. Many sites work, but not all, and the project's own WPT dashboard shows where coverage stands. A site that leans on an unimplemented API may break or crash.
- Missing features. As of now, things like file uploads, multi-tab contexts, clipboard, and geolocation or network-condition emulation aren't supported.
Conclusion
In this article, we went through the architecture behind Lightpanda and why dropping the rendering pipeline, choosing Zig, and building zigdom make it faster than Chromium-based headless browsers.
If your workload is scraping, extraction, or agent automation that never needs pixels, it's worth a look. It is still beta, so the honest way to try it is to point one existing Puppeteer or Playwright script at it and see what breaks: the connect call is a one-line change, and switching back is just as easy.