Lightpanda, a Zig headless browser, is fast because it does not render
Contents
Lightpanda, which climbed to the top of GitHub Trending, is a headless browser written from scratch in Zig. It is not a Chromium fork and it is not a WebKit patch set. It is a clean-slate implementation focused on AI agents and web scraping.
Its biggest selling point is simple: it never renders. By skipping CSS parsing, image decoding, GPU compositing, and font rendering, it claims to be 11x faster than Chrome Headless and to use one-ninth the memory.
This article walks through Lightpanda’s architecture and then shows how to build a practical three-layer stack with Playwright and Browser Use.
Architecture
| Layer | Technology |
|---|---|
| HTTP | libcurl |
| HTML parsing | html5ever (from Mozilla/Servo, via Rust FFI) |
| DOM | Native Zig implementation |
| JavaScript | V8 |
| Protocol | CDP WebSocket |
| Rendering | None |
The DOM and network layers are implemented in Zig, while JavaScript execution is delegated to V8. HTML parsing uses html5ever from Servo through Rust FFI.
Because the browser exposes a CDP (Chrome DevTools Protocol) WebSocket server, existing Puppeteer and Playwright scripts can usually run unchanged. Point the client at Lightpanda and the switch from Chrome is done.
Benchmarks
The benchmark below fetches 100 pages from Puppeteer on an AWS EC2 m5.large instance.
| Metric | Chrome Headless | Lightpanda | Difference |
|---|---|---|---|
| Runtime | 25.2s | 2.3s | 11x faster |
| Peak memory | 207MB | 24MB | 1/9 |
| Startup time | A few seconds | Almost immediate | - |
| Concurrent instances on 8GB RAM | 15 | 140 | 9.3x |
The win is simply the result of removing rendering overhead. One reported deployment cut monthly infrastructure cost from 1,800, an 82% reduction.
Implemented features
- JavaScript execution, DOM APIs, Ajax via XHR/Fetch
- Clicking, form input, cookies
- Custom HTTP headers and proxy support
- Network interception
robots.txt-compliant mode via--obey_robots
How it relates to Playwright and Browser Use
The question “Which is better, Lightpanda or Playwright?” is the wrong one. They live at different layers.
graph TD
A["Browser Use<br/>AI agent layer"] --> B["Playwright / Puppeteer<br/>Browser automation layer"]
B --> C["Lightpanda / Chrome<br/>Browser engine"]
- Lightpanda = browser engine, the Chrome replacement
- Playwright = browser automation library that controls a browser through CDP or WebDriver
- Browser Use = AI agent layer where the LLM decides what to do through Playwright
In other words, Playwright is the client that can sit on top of Lightpanda, and Browser Use can sit on top of Playwright. In theory, the stack can be Browser Use -> Playwright -> Lightpanda.
Feature comparison
| Item | Lightpanda | Playwright | Browser Use |
|---|---|---|---|
| Role | Headless browser engine | Browser automation framework | AI browser agent |
| Language | Zig | Node.js / Python / .NET / Java | Python |
| LLM integration | None | None | Core feature |
| Rendering | No | Chromium / Firefox / WebKit | Real browser through Playwright |
| Natural-language instructions | No | No | Yes |
| Screenshots | No | Yes | Yes |
| License | AGPL-3.0 | Apache-2.0 | MIT |
Browser Use’s control loop
graph TD
A["Task input<br/>Natural-language instruction"] --> B["State acquisition<br/>DOM snapshot + screenshot"]
B --> C["LLM reasoning<br/>Choose the next action"]
C --> D["Action execution<br/>Click, type, scroll"]
D --> E{Task done?}
E -->|No| B
E -->|Yes| F["Return result"]
Browser Use supports major LLMs such as GPT-4, Claude, Gemini, and Llama, and connects through LangChain. It has also raised $17M from Y Combinator and Felicis Ventures and now has more than 81,000 GitHub stars.
Building the three-layer stack
First, install the dependencies:
pip install browser-use langchain-anthropic playwright
playwright install chromium # install Chromium as a fallback
Set ANTHROPIC_API_KEY before running Browser Use so it can call Claude through LangChain.
Start Lightpanda
Run Lightpanda as a CDP server.
# Download the binary (Linux x86_64 example)
curl -LO https://github.com/lightpanda-io/browser/releases/latest/download/x86_64-linux.tar.gz
tar xzf x86_64-linux.tar.gz
# Start the CDP server (default port 9222)
./lightpanda serve --host 127.0.0.1 --port 9222
The WebSocket endpoint will be available at ws://127.0.0.1:9222. A Docker image is also provided.
docker run -p 9222:9222 ghcr.io/nichochar/lightpanda-docker
On macOS and Windows, Docker is the easiest path. As of March 2026, the official binary is only shipped for Linux x86_64.
Connect Playwright to Lightpanda
Playwright’s CDP connection is the only change you need.
from playwright.async_api import async_playwright
import asyncio
async def scrape_with_lightpanda():
async with async_playwright() as p:
browser = await p.chromium.connect_over_cdp("ws://127.0.0.1:9222")
context = browser.contexts[0]
page = context.pages[0] if context.pages else await context.new_page()
await page.goto("https://news.ycombinator.com")
items = await page.query_selector_all(".titleline > a")
for item in items[:10]:
print(await item.inner_text(), await item.get_attribute("href"))
await browser.close()
asyncio.run(scrape_with_lightpanda())
Everything except the connection line is standard Playwright. goto(), query_selector_all(), and inner_text() all work normally.
The catch is that page.screenshot() will not work. Lightpanda does not render, so there is no pixel data. Anything that depends on screenshots needs another browser.
Run Browser Use on top of the stack
Browser Use already uses Playwright, so pointing Playwright at Lightpanda is enough to complete the three-layer stack.
from browser_use import Agent, BrowserConfig, Browser
from langchain_anthropic import ChatAnthropic
import asyncio
browser = Browser(BrowserConfig(cdp_url="ws://127.0.0.1:9222"))
llm = ChatAnthropic(model="claude-sonnet-4-20250514")
agent = Agent(
task="Collect the top 5 Hacker News article titles and URLs",
llm=llm,
browser=browser,
)
result = asyncio.run(agent.run())
print(result)
The flow looks like this:
graph TD
A["Agent.run()<br/>start task"] --> B["Browser Use<br/>sends actions to Playwright"]
B --> C["Playwright<br/>connects to Lightpanda via CDP"]
C --> D["Lightpanda<br/>fetches pages and builds DOM"]
D --> E["DOM data returned<br/>text + structure"]
E --> F["LLM decides the next action"]
F --> G{Task done?}
G -->|No| B
G -->|Yes| H["Return result to the user"]
Browser Use normally sends screenshots and DOM to the LLM. With Lightpanda, the screenshot is unavailable, so DOM-only decision making becomes the default.
Scenario 1: monitoring tech news and sending Slack alerts
A browser agent can visit multiple news sites, search for keywords, and send a Slack summary.
graph LR
A["cron"] --> B["Browser Use<br/>visits news sites"]
B --> C["Playwright<br/>via CDP"]
C --> D["Lightpanda<br/>gets DOM"]
D --> E["LLM summarizes"]
E --> F["Slack webhook"]
from browser_use import Agent, BrowserConfig, Browser
from langchain_anthropic import ChatAnthropic
import asyncio, json, urllib.request
SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
WATCH_KEYWORDS = ["Zig", "WASM", "Rust", "headless browser"]
browser = Browser(BrowserConfig(cdp_url="ws://127.0.0.1:9222"))
llm = ChatAnthropic(model="claude-sonnet-4-20250514")
async def monitor_news():
agent = Agent(
task="""
Visit the following sites in order and find articles related to
the keywords below.
Sites:
- https://news.ycombinator.com
- https://lobste.rs
- https://www.publickey1.jp
Return a JSON array:
[
{
"site": "site name",
"title": "article title",
"url": "article URL",
"keyword": "matched keyword",
"summary": "one-line summary"
}
]
If there are no matches, return [].
""",
llm=llm,
browser=browser,
)
return await agent.run()
def send_to_slack(articles: list):
if not articles:
return
blocks = []
for article in articles:
blocks.append({
"type": "section",
"text": {"type": "mrkdwn", "text": f"*<{article['url']}|{article['title']}>*\n_{article['site']}_ | keyword: {article['keyword']}\n{article['summary']}"},
})
req = urllib.request.Request(
SLACK_WEBHOOK_URL,
data=json.dumps({"blocks": blocks}).encode(),
headers={"Content-Type": "application/json"},
)
urllib.request.urlopen(req)
Put that on a 30-minute cron schedule:
*/30 * * * * cd /path/to/project && python news_monitor.py >> /var/log/news_monitor.log 2>&1
Compared with Chrome Headless, which can take 30 to 60 seconds for three sites, Lightpanda usually finishes in a few seconds.
Scenario 2: price comparison across e-commerce sites
A second use case is gathering prices for the same product from several stores and writing the result to CSV.
graph TD
A["compare_prices(product)"] --> B["Browser Use<br/>search Amazon"]
B --> C["pick first result<br/>and extract price"]
C --> D["Browser Use<br/>search Yahoo Shopping"]
D --> E["pick first result<br/>and extract price"]
E --> F["Browser Use<br/>search Yodobashi"]
F --> G["pick first result<br/>and extract price"]
G --> H["aggregate into JSON<br/>write CSV"]
from browser_use import Agent, BrowserConfig, Browser
from langchain_anthropic import ChatAnthropic
import asyncio, csv, json
from datetime import datetime
browser = Browser(BrowserConfig(cdp_url="ws://127.0.0.1:9222"))
llm = ChatAnthropic(model="claude-sonnet-4-20250514")
async def compare_prices(product_name: str) -> list[dict]:
agent = Agent(
task=f"""
Check the price of "{product_name}" on the following sites.
For each site:
1. Open the site
2. Search for "{product_name}"
3. Click the first product
4. Extract the product name, price, and product page URL
Sites:
- https://www.amazon.co.jp
- https://shopping.yahoo.co.jp
- https://www.yodobashi.com
Return a JSON array.
If search or price extraction fails, skip that site.
""",
llm=llm,
browser=browser,
)
result = await agent.run()
text = result.final_result()
try:
return json.loads(text[text.index("["):text.rindex("]")+1])
except Exception:
return []
async def batch_compare(products: list[str], output_csv: str):
all_results = []
for product in products:
results = await compare_prices(product)
for r in results:
r["search_query"] = product
r["checked_at"] = datetime.now().isoformat()
all_results.extend(results)
if all_results:
with open(output_csv, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["search_query", "site", "product_name", "price", "url", "checked_at"])
writer.writeheader()
writer.writerows(all_results)
Lightpanda wins here because it does not waste time loading images, CSS, or ads.
Scenario 3: collecting and filtering job postings
The third scenario is a daily job collector that stores matches in JSONL and notifies Slack only when there are new ones.
graph TD
A["daily_job_check"] --> B["Browser Use<br/>visit job sites"]
B --> C["Playwright + Lightpanda<br/>get search-result DOM"]
C --> D["LLM evaluates each listing"]
D --> E["filter out already seen IDs"]
E --> F["append to JSONL"]
F --> G["notify Slack if new jobs exist"]
from browser_use import Agent, BrowserConfig, Browser
from langchain_anthropic import ChatAnthropic
import asyncio, json
from pathlib import Path
browser = Browser(BrowserConfig(cdp_url="ws://127.0.0.1:9222"))
llm = ChatAnthropic(model="claude-sonnet-4-20250514")
JOBS_FILE = Path("collected_jobs.jsonl")
async def collect_jobs() -> list[dict]:
agent = Agent(
task="""
Search for "Python remote" on Green Japan and collect the first page of jobs.
For each job, return title, company, url, salary, tags, and location.
Return JSON only.
""",
llm=llm,
browser=browser,
)
result = await agent.run()
text = result.final_result()
return json.loads(text[text.index("["):text.rindex("]")+1])
When Lightpanda falls back to Chrome
Some sites still need full rendering. A simple fallback rule is to try Lightpanda first and switch to Chrome Headless when needed.
NEEDS_CHROME = ["instagram.com", "twitter.com", "maps.google.com", "figma.com"]
async def get_browser_for(url: str) -> Browser:
"""Return Lightpanda or Chrome Headless based on the URL."""
if any(domain in url for domain in NEEDS_CHROME):
return Browser(BrowserConfig(headless=True))
return Browser(BrowserConfig(cdp_url="ws://127.0.0.1:9222"))
You can also implement a retry strategy: try Lightpanda, and if it fails, retry with Chrome Headless.
Constraints
| Constraint | Cause | Mitigation |
|---|---|---|
| No screenshots | Lightpanda does not render | Use Browser Use in DOM-text mode |
| Some SPA sites do not work | Web API coverage is still incomplete | Fallback to Chrome Headless for those sites |
| No PDF generation | No rendering engine | Use wkhtmltopdf or similar |
| Sites that depend on WebSocket behavior | Still under active development | Use Chrome Headless |
| AGPL-3.0 license | Lightpanda’s license | Use the cloud service if you need to avoid self-hosting obligations |
The biggest trade-off is that Browser Use can no longer decide actions from screenshots, such as “click the button on the right based on color and position.” If the target can be identified from the DOM structure alone, Lightpanda is fine. If the UI relies on icon-only buttons or Canvas rendering, it is not.
Where it fits and where it doesn’t
| Use case | Fit | Why |
|---|---|---|
| Large-scale page extraction and crawling | Good fit | No rendering and low memory use |
| AI-agent web actions based on text | Good fit | DOM/text is enough |
| CI/CD headless tests without visual assertions | Good fit | Fast startup and easy parallelism |
| Parallel processing on low-resource machines | Good fit | One-ninth the memory means more concurrency |
| Screenshot-based AI automation | Poor fit | There is no pixel data |
| Visual regression testing | Poor fit | Same reason |
| Complex SPAs | Poor fit | They may rely on missing web APIs |
Current status and caveats
As of March 2026, Lightpanda is in beta. It has more than 23,000 GitHub stars and raised pre-seed funding in 2025.
Things to keep in mind:
| Item | Notes |
|---|---|
| Beta status | It works on many sites, but crashes and errors can still happen |
| Web API coverage | A browser has hundreds of APIs, so full coverage will take time |
| AGPL-3.0 | Self-hosting in a SaaS product may require source disclosure; the cloud service avoids that |
| Playwright compatibility | Behavior may change when new APIs are added |
It is still rough around the edges, but with a fallback path in place you can try it right now. Switching from Chrome to Lightpanda is a one-line change with connect_over_cdp(), and switching back is just as easy. The project is basically the straightforward idea that “if you only need text, you do not need to render pixels” turned into working software.