技術 約16分で読めます

Zig製ヘッドレスブラウザLightpandaはChromeの11倍速でレンダリングしない

GitHub Trending 1位に上がってきたLightpandaは、Zig言語でゼロから書かれたヘッドレスブラウザだ。Chromiumフォークでもなく、WebKitパッチでもない完全な新規実装で、AIエージェントやWebスクレイピングに特化している。

最大の特徴は「レンダリングを一切しない」こと。CSS解析、画像デコード、GPU合成、フォント描画など、人間が画面を見るために必要な処理をすべて省くことで、Chrome Headlessの11倍速・メモリ9分の1という数字を叩き出している。

この記事ではLightpandaのアーキテクチャを整理して、Playwright・Browser Useとの3層スタックを実際にセットアップするところまでやる。

アーキテクチャ

レイヤー技術
HTTP通信libcurl
HTML解析html5ever(Mozilla/Servo由来、RustからFFI)
DOMZig独自実装
JavaScriptV8エンジン
プロトコルCDP(Chrome DevTools Protocol)WebSocket
レンダリングなし

DOMとネットワーク層をZigで実装し、JavaScript実行はV8に委譲する構成。HTML解析にはMozillaのServoプロジェクト由来のhtml5everをRust FFI経由で使っている。

ブラウザとしてCDP(Chrome DevTools Protocol)のWebSocketサーバーを提供するので、既存のPuppeteerやPlaywrightのスクリプトがそのまま動く。接続先URLをLightpandaに向けるだけでChromeからの移行が完了する、というのが売り文句。

ベンチマーク

AWS EC2 m5.largeでPuppeteerから100ページを取得するテスト。

指標Chrome HeadlessLightpanda
実行時間25.2秒2.3秒11倍高速
メモリピーク207MB24MB9分の1
起動時間数秒ほぼ即時-
同時実行数(8GB RAM)15インスタンス140インスタンス9.3倍

レンダリングを省いた分がそのまま速度とメモリの差になっている。ある事例ではインフラコストが月額10,200から10,200から1,800に削減(82%減)されたと報告されている。

実装済み機能

  • JavaScript実行、DOM API、Ajax(XHR/Fetch)
  • クリック、フォーム入力、Cookie
  • カスタムHTTPヘッダー、プロキシ対応
  • ネットワークインターセプト
  • robots.txt準拠モード(--obey_robotsオプション)

Playwright・Browser Useとの関係

「LightpandaとPlaywrightはどっちがいいの?」という質問自体がずれている。この3つはレイヤーが違う。

graph TD
    A["Browser Use<br/>AIエージェント層"] --> B["Playwright / Puppeteer<br/>ブラウザ操作ライブラリ"]
    B --> C["Lightpanda / Chrome<br/>ブラウザエンジン"]
  • Lightpanda = ブラウザエンジン(Chromeの代替ポジション)
  • Playwright = ブラウザ操作ライブラリ(CDPやWebDriverを通じてブラウザを制御する)
  • Browser Use = AIエージェント層(LLMがPlaywrightを通じてブラウザを操作する)

つまりPlaywrightは「Lightpandaの上で動く」クライアントであり、Browser Useは「Playwrightの上で動く」オーケストレーターだ。理論上、Browser Use → Playwright → Lightpandaというスタックが組める。

各ツールの詳細比較

項目LightpandaPlaywrightBrowser Use
役割ヘッドレスブラウザエンジンブラウザ自動化フレームワークAIブラウザエージェント
言語ZigNode.js / Python / .NET / JavaPython
LLM連携なしなしコア機能(LLMが操作判断)
レンダリングしないChromium / Firefox / WebKitPlaywright経由で実ブラウザ
自然言語指示不可不可可能
スクリーンショット不可可能可能(操作判断に使用)
ライセンスAGPL-3.0Apache-2.0MIT

Browser Useの動作ループ

Browser Useは以下のサイクルでタスクを実行する。

graph TD
    A["タスク受け取り<br/>自然言語の指示"] --> B["状態取得<br/>DOMスナップショット +<br/>スクリーンショット"]
    B --> C["LLM推論<br/>次のアクションを決定"]
    C --> D["アクション実行<br/>クリック・入力・スクロール"]
    D --> E{タスク完了?}
    E -->|No| B
    E -->|Yes| F["結果返却"]

GPT-4、Claude、Gemini、Llamaなど主要LLMに対応し、LangChain経由で接続する。Y CombinatorとFelicis Venturesから$17Mのシード資金を調達しており、GitHubスター数は81,000を超えている。

3層スタックの環境構築

Browser Use → Playwright → Lightpandaの3層スタックを実際に構築する。まず環境を整えるところから。

依存パッケージのインストール

pip install browser-use langchain-anthropic playwright
playwright install chromium  # フォールバック用にChromiumも入れておく

ANTHROPIC_API_KEY環境変数にClaudeのAPIキーを設定しておく。Browser UseがLangChain経由でLLMを呼ぶときに使う。

Lightpandaの起動

LightpandaをCDPサーバーとして起動する。

# Lightpandaのバイナリを取得(Linux x86_64の例)
curl -LO https://github.com/lightpanda-io/browser/releases/latest/download/x86_64-linux.tar.gz
tar xzf x86_64-linux.tar.gz

# CDPサーバーとして起動(デフォルトでport 9222)
./lightpanda serve --host 127.0.0.1 --port 9222

起動するとCDP WebSocketエンドポイントがws://127.0.0.1:9222で待ち受ける。Dockerイメージも提供されている。

docker run -p 9222:9222 ghcr.io/nichochar/lightpanda-docker

macOSやWindowsで動かす場合はDocker経由が手軽。Lightpanda本体のバイナリは2026年3月時点でLinux x86_64のみ公式配布されている。

PlaywrightからLightpandaに接続する

PlaywrightのCDP接続機能を使うと、ChromeをLightpandaに差し替えられる。変更点は接続先URLだけ。

from playwright.async_api import async_playwright
import asyncio

async def scrape_with_lightpanda():
    async with async_playwright() as p:
        # Chrome起動の代わりにLightpandaのCDPエンドポイントに接続
        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")

        # DOM操作は通常のPlaywrightと同じ
        items = await page.query_selector_all(".titleline > a")
        for item in items[:10]:
            title = await item.inner_text()
            href = await item.get_attribute("href")
            print(f"{title}: {href}")

        await browser.close()

asyncio.run(scrape_with_lightpanda())

通常のPlaywrightスクリプトとの違いはconnect_over_cdp()で接続する1行だけ。page.goto()query_selector_all()inner_text()などのDOM操作APIはそのまま使える。

ただしpage.screenshot()は動かない。Lightpandaはレンダリングしないのでピクセルデータを生成できない。スクリーンショットに依存するテストや処理は別のブラウザを使う必要がある。

Browser Useから3層スタックで動かす

Browser Useは内部でPlaywrightを使っているので、Playwrightの接続先をLightpandaに向ければ3層スタックが成立する。

from browser_use import Agent, BrowserConfig, Browser
from langchain_anthropic import ChatAnthropic

# Lightpandaに接続するBrowser設定
config = BrowserConfig(
    cdp_url="ws://127.0.0.1:9222",  # LightpandaのCDPエンドポイント
)

browser = Browser(config=config)

# LLMにClaudeを使用
llm = ChatAnthropic(model="claude-sonnet-4-20250514")

agent = Agent(
    task="Hacker Newsのトップ5記事のタイトルとURLを取得して",
    llm=llm,
    browser=browser,
)

import asyncio
result = asyncio.run(agent.run())
print(result)

このコードが実行されると、以下の流れで処理が進む。

graph TD
    A["Agent.run()<br/>タスクを開始"] --> B["Browser Use<br/>Playwrightに操作指示"]
    B --> C["Playwright<br/>CDPでLightpandaに接続"]
    C --> D["Lightpanda<br/>ページ取得 + DOM構築"]
    D --> E["DOM情報を返却<br/>テキスト + 構造"]
    E --> F["LLMが判断<br/>次のアクションを決定"]
    F --> G{タスク完了?}
    G -->|No| B
    G -->|Yes| H["結果をユーザーに返却"]

Browser UseはデフォルトでスクリーンショットとDOMの両方をLLMに送信する。Lightpandaを使う場合はスクリーンショットが取れないので、DOMテキストだけで判断するモードが前提になる。

ここから先は3層スタックで実際にスクレイピングしてみる。

シナリオ1: 技術ニュースの定期監視とSlack通知

複数のニュースソースを巡回して、特定キーワードに関する新着記事をSlackに流すエージェント。

graph LR
    A["cronで定期実行"] --> B["Browser Use<br/>ニュースサイト巡回"]
    B --> C["Playwright<br/>CDP経由で操作"]
    C --> D["Lightpanda<br/>DOM取得"]
    D --> E["LLMが要約生成"]
    E --> F["Slack Webhook<br/>で通知"]
from browser_use import Agent, BrowserConfig, Browser
from langchain_anthropic import ChatAnthropic
import asyncio
import json
import urllib.request

SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
WATCH_KEYWORDS = ["Zig", "WASM", "Rust", "ヘッドレスブラウザ"]

config = BrowserConfig(cdp_url="ws://127.0.0.1:9222")
browser = Browser(config=config)
llm = ChatAnthropic(model="claude-sonnet-4-20250514")


async def monitor_news():
    sites = [
        "https://news.ycombinator.com",
        "https://lobste.rs",
        "https://www.publickey1.jp",
    ]

    keywords_str = "、".join(WATCH_KEYWORDS)

    agent = Agent(
        task=f"""
以下のサイトを順番に訪問し、トップページに表示されている記事の中から
{keywords_str}」に関連するものを探してください。

対象サイト:
{chr(10).join(f"- {url}" for url in sites)}

各サイトで関連記事が見つかったら、以下の形式でJSON配列にまとめてください:
[
  {{
    "site": "サイト名",
    "title": "記事タイトル",
    "url": "記事URL",
    "keyword": "マッチしたキーワード",
    "summary": "1行の要約"
  }}
]

関連記事がなければ空配列 [] を返してください。
""",
        llm=llm,
        browser=browser,
    )
    result = await agent.run()
    return result


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"
                    f"_{article['site']}_ | キーワード: {article['keyword']}\n"
                    f"{article['summary']}"
                ),
            },
        })

    payload = json.dumps({"blocks": blocks}).encode()
    req = urllib.request.Request(
        SLACK_WEBHOOK_URL,
        data=payload,
        headers={"Content-Type": "application/json"},
    )
    urllib.request.urlopen(req)


async def main():
    result = await monitor_news()

    # Browser Useの結果からJSON部分を抽出
    # result.final_result() が文字列を返すので、その中のJSON配列をパース
    result_text = result.final_result()
    try:
        # JSON配列部分を探して抽出
        start = result_text.index("[")
        end = result_text.rindex("]") + 1
        articles = json.loads(result_text[start:end])
        send_to_slack(articles)
        print(f"{len(articles)}件の記事をSlackに通知しました")
    except (ValueError, json.JSONDecodeError):
        print("関連記事なし、またはパース失敗")


asyncio.run(main())

これをcrontab -eで30分おきに回す。

*/30 * * * * cd /path/to/project && python news_monitor.py >> /var/log/news_monitor.log 2>&1

Chrome Headlessで3サイト巡回すると1回あたり30〜60秒かかるところが、Lightpandaなら数秒で終わる。cron間隔を短くしても負荷が問題にならない。

シナリオ2: ECサイト横断の価格比較

複数のECサイトから特定商品の価格を収集して、CSVに吐き出すエージェント。3サイト巡回で検索→商品ページ遷移→価格取得まで一気に自然言語で指示できる。

graph TD
    A["compare_prices 呼び出し<br/>商品名を渡す"] --> B["Browser Use<br/>Amazon検索"]
    B --> C["検索結果から商品を選択<br/>価格を取得"]
    C --> D["Browser Use<br/>Yahoo!ショッピング検索"]
    D --> E["検索結果から商品を選択<br/>価格を取得"]
    E --> F["Browser Use<br/>ヨドバシ検索"]
    F --> G["検索結果から商品を選択<br/>価格を取得"]
    G --> H["結果をJSON集約<br/>CSVに出力"]
from browser_use import Agent, BrowserConfig, Browser
from langchain_anthropic import ChatAnthropic
import asyncio
import json
import csv
from datetime import datetime

config = BrowserConfig(cdp_url="ws://127.0.0.1:9222")
browser = Browser(config=config)
llm = ChatAnthropic(model="claude-sonnet-4-20250514")


async def compare_prices(product_name: str) -> list[dict]:
    agent = Agent(
        task=f"""
{product_name}」の価格を以下の3サイトで調べてください。

各サイトで以下の手順を実行:
1. サイトにアクセス
2. 検索ボックスに「{product_name}」と入力して検索
3. 検索結果の最初の商品をクリック
4. 商品名、価格(税込)、商品ページURLを取得

対象サイト:
- https://www.amazon.co.jp
- https://shopping.yahoo.co.jp
- https://www.yodobashi.com

結果を以下のJSON配列で返してください:
[
  {{
    "site": "Amazon",
    "product_name": "正式な商品名",
    "price": 38000,
    "url": "商品ページのURL"
  }}
]

価格はカンマや円記号を除いた数値で入れてください。
検索できなかった場合や価格が取得できなかった場合は、
そのサイトの結果をスキップしてください。
""",
        llm=llm,
        browser=browser,
    )
    result = await agent.run()

    result_text = result.final_result()
    try:
        start = result_text.index("[")
        end = result_text.rindex("]") + 1
        return json.loads(result_text[start:end])
    except (ValueError, json.JSONDecodeError):
        return []


async def batch_compare(products: list[str], output_csv: str):
    """複数商品を順番に比較してCSVに出力"""
    all_results = []

    for product in products:
        print(f"検索中: {product}")
        results = await compare_prices(product)
        for r in results:
            r["search_query"] = product
            r["checked_at"] = datetime.now().isoformat()
        all_results.extend(results)

    # CSV出力
    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)
        print(f"{output_csv}{len(all_results)} 件書き出しました")


# 使用例: 複数商品をまとめて比較
products = [
    "Sony WH-1000XM5",
    "Apple AirPods Pro 2",
    "Anker Soundcore Space A40",
]

asyncio.run(batch_compare(products, "price_comparison.csv"))

3商品 x 3サイト = 9ページを巡回する。Chrome Headlessだと商品画像やレコメンド広告まで全部読み込むので1商品あたり20〜30秒。Lightpandaなら画像もCSSも読み込まないから、DOM取得だけなら数秒で済む。商品数が増えるほど差が効いてくる。

出力されるCSVはこんな感じになる。

search_query,site,product_name,price,url,checked_at
Sony WH-1000XM5,Amazon,ソニー ワイヤレスノイズキャンセリングヘッドホン WH-1000XM5,38000,https://www.amazon.co.jp/dp/...,2026-03-22T15:30:00
Sony WH-1000XM5,Yahoo!ショッピング,SONY WH-1000XM5 ブラック,36800,https://store.shopping.yahoo.co.jp/...,2026-03-22T15:30:15
Sony WH-1000XM5,ヨドバシ,ソニー WH-1000XM5 BM,41800,https://www.yodobashi.com/product/...,2026-03-22T15:30:25

シナリオ3: 求人情報の定期収集とフィルタリング

求人サイトから条件に合う案件を自動で集めて、JSONLに蓄積していくエージェント。毎日回して差分だけ通知する構成にする。

graph TD
    A["daily_job_check 実行"] --> B["Browser Use<br/>求人サイトを巡回"]
    B --> C["Playwright + Lightpanda<br/>検索結果ページのDOM取得"]
    C --> D["LLMが各求人を評価<br/>条件にマッチするか判定"]
    D --> E["新規求人だけ抽出<br/>既存IDと突合"]
    E --> F["JSONLに追記"]
    F --> G["新着があればSlack通知"]
from browser_use import Agent, BrowserConfig, Browser
from langchain_anthropic import ChatAnthropic
import asyncio
import json
from pathlib import Path
from datetime import datetime

config = BrowserConfig(cdp_url="ws://127.0.0.1:9222")
browser = Browser(config=config)
llm = ChatAnthropic(model="claude-sonnet-4-20250514")

JOBS_FILE = Path("collected_jobs.jsonl")


def load_existing_urls() -> set[str]:
    """既に収集済みの求人URLを取得"""
    urls = set()
    if JOBS_FILE.exists():
        for line in JOBS_FILE.read_text().splitlines():
            if line.strip():
                job = json.loads(line)
                urls.add(job["url"])
    return urls


async def collect_jobs() -> list[dict]:
    agent = Agent(
        task="""
以下の求人サイトで「Python リモート」の条件で検索し、
表示された求人の情報を収集してください。

対象サイト:
- https://www.green-japan.com (検索ボックスに「Python リモート」と入力)

各求人について以下の情報を取得:
- title: 求人タイトル
- company: 企業名
- url: 求人詳細ページのURL
- salary: 年収範囲(記載があれば)
- tags: 技術タグのリスト(記載があれば)
- location: 勤務地

最初のページに表示されている求人(最大20件)を対象にしてください。
結果はJSON配列で返してください。
""",
        llm=llm,
        browser=browser,
    )
    result = await agent.run()

    result_text = result.final_result()
    try:
        start = result_text.index("[")
        end = result_text.rindex("]") + 1
        return json.loads(result_text[start:end])
    except (ValueError, json.JSONDecodeError):
        return []


async def daily_job_check():
    existing_urls = load_existing_urls()
    jobs = await collect_jobs()

    new_jobs = [j for j in jobs if j.get("url") not in existing_urls]

    if new_jobs:
        with JOBS_FILE.open("a", encoding="utf-8") as f:
            for job in new_jobs:
                job["collected_at"] = datetime.now().isoformat()
                f.write(json.dumps(job, ensure_ascii=False) + "\n")

        print(f"新着 {len(new_jobs)} 件を追加しました:")
        for job in new_jobs:
            print(f"  - {job['title']} @ {job.get('company', '不明')}")
    else:
        print("新着なし")

    return new_jobs


asyncio.run(daily_job_check())

これもcrontabで毎朝回す。collected_jobs.jsonlにはJSONL形式で蓄積されていくので、あとからjqで集計したりPandasで分析したりできる。

# 蓄積データの確認
cat collected_jobs.jsonl | python -m json.tool --json-lines | head -20

# 年収でフィルタ(jq使用)
cat collected_jobs.jsonl | jq -r 'select(.salary != null) | "\(.title) - \(.salary)"'

求人サイトは画像やJavaScriptが重いページが多いので、DOM取得だけで済むLightpandaとの相性がいい。ただし検索フォームのJavaScript実装が凝っているサイト(リアルタイムサジェスト、無限スクロールなど)ではLightpandaのWeb APIカバレッジが足りなくて動かないことがある。その場合は次のセクションで説明するフォールバック構成に切り替える。

ハイブリッド構成: Lightpanda + Chrome Headlessの自動切り替え

3つのシナリオを動かしていると、サイトによってLightpandaでは動かないケースが出てくる。重いSPAや、Canvas/WebGLを使ったUI、スクリーンショットが必要な処理など。

実運用では「基本はLightpandaで高速に回し、ダメなサイトだけChrome Headlessにフォールバックする」ハイブリッド構成が現実的。

from browser_use import BrowserConfig, Browser

# レンダリングが必要なサイトのリスト
NEEDS_CHROME = [
    "instagram.com",
    "twitter.com",
    "maps.google.com",
    "figma.com",
]


async def get_browser_for(url: str) -> Browser:
    """URLに応じてLightpandaかChrome Headlessを返す"""
    if any(domain in url for domain in NEEDS_CHROME):
        # Chrome Headless(レンダリング付き)
        return Browser(BrowserConfig(headless=True))
    else:
        # Lightpanda(高速・軽量)
        return Browser(BrowserConfig(cdp_url="ws://127.0.0.1:9222"))

フォールバックリストは手動管理でもいいが、もう少し賢くやるなら「Lightpandaで試して、エラーが出たらChrome Headlessで再試行」という方式も組める。

async def scrape_with_fallback(task: str, url: str, llm) -> str:
    """Lightpandaで試行し、失敗したらChrome Headlessにフォールバック"""
    try:
        browser = Browser(BrowserConfig(cdp_url="ws://127.0.0.1:9222"))
        agent = Agent(task=task, llm=llm, browser=browser)
        result = await agent.run()
        return result.final_result()
    except Exception as e:
        print(f"Lightpanda失敗 ({url}): {e}")
        print("Chrome Headlessにフォールバック")
        browser = Browser(BrowserConfig(headless=True))
        agent = Agent(task=task, llm=llm, browser=browser)
        result = await agent.run()
        return result.final_result()

この方式だとLightpandaのWeb APIカバレッジが広がるにつれて、自動的にChrome Headless側に落ちるサイトが減っていく。

3層スタックの制約

制約原因対処
スクリーンショット不可LightpandaがレンダリングしないBrowser UseをDOMテキストモードで使う
一部SPAで動作しないWeb APIカバレッジが不完全該当サイトだけChrome Headlessにフォールバック
PDF生成不可レンダリングエンジンがない別途wkhtmltopdf等を使う
WebSocket通信のサイト実装途上Chrome Headlessを使う
AGPL-3.0ライセンスLightpanda本体の制約SaaSに組み込む場合はクラウドサービスを利用

Browser Useのスクリーンショットベースの操作判断(ボタンの位置や色を見てクリック先を決める等)は使えなくなるのが最大のトレードオフ。テキスト構造から操作対象を特定できるサイトなら問題ないが、アイコンだけのボタンやCanvas描画のUIは操作できない。

Lightpandaが向いているケース・向かないケース

ユースケース向き不向き理由
大量ページのデータ抽出・クローリング向いているレンダリング不要、メモリも軽い
AIエージェントのテキストベースWeb操作向いているDOM/テキスト取得だけなら十分
CI/CDでのヘッドレステスト(視覚テスト不要)向いている起動が速く並列しやすい
低リソース環境での並行処理向いているメモリ9分の1で同時実行数を稼げる
スクリーンショットベースのAI操作向かないレンダリングしないのでピクセルデータがない
ビジュアルリグレッションテスト向かない同上
複雑なSPA向かない未実装のWeb APIに依存すると動かない

現在のステータスと注意点

2026年3月時点でBeta。GitHubスター23,000超、2025年中頃にプレシード資金調達済み。

注意すべき点。

項目内容
Beta段階多くのWebサイトで動作するが、クラッシュやエラーは起きうる
Web APIカバレッジブラウザのWeb APIは数百あり、全対応には時間がかかる
AGPL-3.0ライセンスSaaSに組み込む場合はソースコード公開義務あり。クラウドサービス(cloud.lightpanda.io)で回避可能
Playwright互換新しいWeb API実装時に既存スクリプトの挙動が変わることがある

Betaなので安定性はまだ怪しいが、フォールバック構成を組んでおけば今すぐ試せる。connect_over_cdp()の1行でChromeから切り替わるので、ダメだったら戻すのも一瞬。テキストを抜くだけならレンダリングは要らない、というシンプルな話をちゃんと実装したプロジェクトだと思う。