Tech 11 min read

Lightpanda, a Zig headless browser, is fast because it does not render

IkesanContents

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

LayerTechnology
HTTPlibcurl
HTML parsinghtml5ever (from Mozilla/Servo, via Rust FFI)
DOMNative Zig implementation
JavaScriptV8
ProtocolCDP WebSocket
RenderingNone

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.

MetricChrome HeadlessLightpandaDifference
Runtime25.2s2.3s11x faster
Peak memory207MB24MB1/9
Startup timeA few secondsAlmost immediate-
Concurrent instances on 8GB RAM151409.3x

The win is simply the result of removing rendering overhead. One reported deployment cut monthly infrastructure cost from 10,200to10,200 to 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

ItemLightpandaPlaywrightBrowser Use
RoleHeadless browser engineBrowser automation frameworkAI browser agent
LanguageZigNode.js / Python / .NET / JavaPython
LLM integrationNoneNoneCore feature
RenderingNoChromium / Firefox / WebKitReal browser through Playwright
Natural-language instructionsNoNoYes
ScreenshotsNoYesYes
LicenseAGPL-3.0Apache-2.0MIT

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

ConstraintCauseMitigation
No screenshotsLightpanda does not renderUse Browser Use in DOM-text mode
Some SPA sites do not workWeb API coverage is still incompleteFallback to Chrome Headless for those sites
No PDF generationNo rendering engineUse wkhtmltopdf or similar
Sites that depend on WebSocket behaviorStill under active developmentUse Chrome Headless
AGPL-3.0 licenseLightpanda’s licenseUse 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 caseFitWhy
Large-scale page extraction and crawlingGood fitNo rendering and low memory use
AI-agent web actions based on textGood fitDOM/text is enough
CI/CD headless tests without visual assertionsGood fitFast startup and easy parallelism
Parallel processing on low-resource machinesGood fitOne-ninth the memory means more concurrency
Screenshot-based AI automationPoor fitThere is no pixel data
Visual regression testingPoor fitSame reason
Complex SPAsPoor fitThey 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:

ItemNotes
Beta statusIt works on many sites, but crashes and errors can still happen
Web API coverageA browser has hundreds of APIs, so full coverage will take time
AGPL-3.0Self-hosting in a SaaS product may require source disclosure; the cloud service avoids that
Playwright compatibilityBehavior 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.