Lightpanda 解析
GitHub ↗
基于 commit e6cffae · 2026-04-13

Lightpanda
从源码读懂这只"快 11 倍"的无头熊猫

全 Zig 手写、非 Chromium 分支的无头浏览器。 用 html5ever 解析 HTML,V8 跑 JS,libcurl 跑网络, 暴露 CDP + MCP 双协议。
这份文档基于 commit e6cffae 的实际源码,带文件:行号引用。

32
CDP 域
217
Web API 文件
20+
MCP 工具
11×
相对 Chrome
ZigRust (html5ever)C (libcurl/BoringSSL) V8AGPL-3.0~9 MB 源码
🎯

不是噱头,但有明确边界

11× 是真的——代价是完全砍掉了 CSS 布局、像素渲染、Canvas 绘制。 Lightpanda 只保留 DOM 正确性、JS 兼容性、网络保真度、语义提取四件事。 用它做批量抓取、SSR 测试、AI Agent 驱动非常合适; 用它做需要截图、视觉校验、重 SPA的场景会翻车。 AGPL-3.0 license 对商用 SaaS 也要留意。

§ 01 / ENTRY POINT

入口与进程模型

一个 main,三种模式:CDP 服务器、单次 fetch、MCP stdio。所有模式共用同一套 Browser 实例。

main.zig
解析 CLI → 装 allocator → 绑 SIGTERM/SIGINT → 分派模式
main.zig:34-170
serve mode
bind 127.0.0.1:9222 → Server.init() → 事件循环
main.zig:92-118
fetch mode
worker 线程跑 lp.fetch(),支持 --wait-until / --dump / --wait-selector
main.zig:119-145
mcp mode
读 stdin 写 stdout,mcp.router.processRequests()
main.zig:147-167

内存策略

  • Debug 构建用 GeneralPurposeAllocator 查泄漏 main.zig:34-44
  • Release 构建用 C allocator,主线程外包 Arena
  • 分 tiny/small/large 三档 Arena 池 App.zig:40

默认参数

  • Bind: 127.0.0.1:9222(默认只绑环回)
  • Inactivity timeout: 10s(1~604800)
  • CDP 消息上限: 512 KB Config.zig:42
  • HTTP 超时: 5000 ms Config.zig:115
  • UA 基线: Lightpanda/1.0 Config.zig:325-339
§ 02 / CDP SERVER

CDP 协议层

Chrome DevTools Protocol 的 Zig 实现。兼容 Puppeteer/Playwright,但只实现了一个子集。路由用 bit-cast 加速。

巧思cdp/CDP.zig:200-262 的分派器按域名字符数分组, 对每一组用 @bitCast(domain[0..N].*) 把字符串整体转成整数,再在整数上 switch, 省掉字符串比较,O(1) 路由。
// 伪代码示意(实际在 CDP.zig:200-262)
switch (domain.len) {
    2 => switch (@bitCast(u16, domain[0..2].*)) { "LP" => ... },
    3 => switch (@bitCast(u24, domain[0..3].*)) { "DOM", "Log", "CSS" => ... },
    4 => ..., 5 => ..., 6 => ..., 7 => ...,
}

已实现的 CDP 域

Target

目标发现/附加

cdp/domains/target.zig

Page

导航、脚本求值、生命周期

cdp/domains/page.zig

DOM

树查询、quads、节点搜索

cdp/domains/dom.zig

Runtime

JS 求值、属性读取、调用栈

cdp/domains/runtime.zig

Network

网络事件、请求/响应捕获

cdp/domains/network.zig

Fetch

请求拦截、响应改写

cdp/domains/fetch.zig

Input

鼠标、键盘、触摸

cdp/domains/input.zig

Log

Console 日志

cdp/domains/log.zig

Storage

Cookie、LocalStorage

cdp/domains/storage.zig

CSS

规则读取(部分)

cdp/domains/css.zig

Browser

版本、窗口边界(多为桩)

cdp/domains/browser.zig

Inspector

状态(桩)

cdp/domains/inspector.zig

Security

安全信息

cdp/domains/security.zig

Emulation

设备模拟(桩——无布局)

cdp/domains/emulation.zig

Accessibility

AXNode 可访问性树

cdp/domains/accessibility.zig

Performance

性能指标

cdp/domains/performance.zig

LP (自扩)

Markdown dump 等

cdp/domains/lp.zig
关键约束cdp/CDP.zig:270-281 强制只允许一个 BrowserContext 同时存在—— Lightpanda 任何时刻只有一个活动上下文、一个 Session、一个活动 Page。 不支持多页面并发。这是为了简化状态管理,但比 Chrome 的标签架构弱。
§ 03 / BROWSER CORE

浏览器核心三件套

Browser / Session / Page 三层结构,加上 Runner 事件循环。

Browser
持有 V8 环境,创建 Session
Browser.zig:43-119
  └─ Session
cookie jar / history / origins / Page 生命周期
Session.zig:109-187
      └─ Page
DOM 树 + frame 层级(iframes)
Page.zig:200-600+
Runner
事件循环: tick HttpClient → runMacrotasks → runMicrotasks
Runner.zig:57-150

导航流程

  • Client 调 Page.navigate(url)(CDP 或直接 API)
  • Page 入队导航
  • Runner.wait() 驱动事件循环直到满足等待条件
  • HTTP 进度 → Parser → JS 任务队列 → 微任务
  • Page 发出 load / DOMContentLoaded / networkidle
  • 通知 CDP 的 Page.navigated

Arena 策略

Session 持有两个 Arena:

  • page_arena — 每次导航重建,销毁快
  • arena — Session 生命周期

Session.zig:116-117 代价:无跨页面复用,但换来干净快速的 teardown

§ 04 / HTML PARSING

HTML 解析 · Zig ↔ Rust FFI

直接用 Mozilla 的 html5ever(Rust 写的 HTML5 spec 实现),通过 C FFI 被 Zig 调用。

为什么不自己写? html5ever 是 Mozilla Firefox 的产线级 HTML parser, 实现了 adoption agency、template 内容模型、script insertion point 等所有奇葩边缘情况。 从零复刻一个正确的 HTML parser 是多人年级别的工程,Lightpanda 直接用就对了。

构建集成

  • build.zig:235-265cargo build 编译 src/html5ever/Cargo.toml
  • 产出 liblitefetch_html5ever.a
  • Zig 侧 mod.addObjectFile(obj) 静态链入
  • Cargo.toml + lib.rs 作为 input 追踪,变化自动重编

C API(Zig 侧声明)

  • html5ever_parse_document()
  • html5ever_parse_document_with_encoding() — charset aware
  • html5ever_parse_fragment() — innerHTML
  • html5ever_streaming_parser_create/feed/finish()
  • browser/parser/html5ever.zig:21-90

Rust → Zig 回调列表

// browser/parser/html5ever.zig:21-40
extern fn createElementCallback(ctx: *Parser, tag: *const u8, attrs: *const Attr, n: usize) *Node;
extern fn appendCallback(ctx: *Parser, parent: *Node, child: *Node) void;
extern fn popCallback(ctx: *Parser, node: *Node) void;
extern fn createCommentCallback(ctx: *Parser, text: *const u8, len: usize) *Node;
extern fn createProcessingInstruction(ctx: *Parser, target: [], data: []) *Node;
extern fn appendDoctypeToDocument(ctx: *Parser, name: [], publicId: [], systemId: []) void;
extern fn getTemplateContentsCallback(ctx: *Parser, node: *Node) *Node;
extern fn reparentChildrenCallback(ctx: *Parser, old: *Node, new: *Node) void;
extern fn addAttrsIfMissingCallback(ctx: *Parser, node: *Node, attrs: []Attr) void;

每个回调在 Zig 端实现为 callconv(.c) fn (ctx: *Parser, ...) Parser.zig:42-176ParsedNode 包装 Node* 加可选 element 数据。 错误通过 Parser.err union 和源码位置归属。

DOM 节点表示browser/webapi/Node.zig):tagged union 包含 Document、DocumentFragment、Element、Text、Comment、PI、DocumentType、CDATA。 Element 带 qualified name、懒创建的 Attribute 节点、双向链表子节点、cached namespace URI。
§ 05 / JS RUNTIME

JS 运行时 · V8 绑定

用 V8 跑 JS,通过代码生成为每个 WebAPI 类型自动产出绑定。Zig 对象和 V8 对象通过 internal field 指针互映。

V8 集成

  • build.zig:213-233 --prebuilt-v8-path 优先,否则从 zig-v8 包从源码编译(~1 小时)
  • browser/js/js.zig:19-72 导出 V8 C API、TypedArray wrapper
  • browser/js/Platform.zig 平台初始化,快照加载
  • browser/js/Env.zig 每页多 Context(支持同源隔离),Global = Window

绑定桥

browser/js/bridge.zig 是代码生成式绑定,对每个 WebAPI 类型自动产出:

  • Constructor(new Element(...)
  • Property accessor(getter/setter)
  • Method
  • Callback dispatch

JS 对象在 V8 internal field 存 Zig 指针,identity 稳定:同一个 Zig 对象永远映射到同一个 V8 对象。

类型转换规则(Value.zig)

Zig 类型V8 类型
boolBoolean
i32 / u32 / f64Number
[]const u8String
*MyTypeObject with internal field = 指针
?Tnull / Object
error!TJS 异常 / 值
调用回传browser/js/Caller.zig):JS 调 Zig 方法 → 从 V8 internal field 取 Zig 指针 → 从 V8 参数数组取参数并转 Zig 类型 → 调 Zig 函数 → 返回值转 V8 value。 Zig error 直接 throw 成 JS 异常。
§ 06 / WEB API

Web API 实现矩阵

src/browser/webapi/217 个 .zig 文件。完整、半实现、缺失三档。

✓ 完整实现

Node/Element Document querySelector(All) classList dataset EventTarget MouseEvent KeyboardEvent LocalStorage SessionStorage Cookie jar URL / URLSearchParams fetch() XMLHttpRequest WebSocket SubtleCrypto FileReader Performance setTimeout MutationObserver Navigation/History CustomElementRegistry ShadowRoot Selection/Range

△ 桩实现

OffscreenCanvas (blob=空) Canvas2D (方法空) CSSStyleSheet (解析但不级联) IntersectionObserver (永远可见) AXNode (部分) Emulation (no-op)

webapi/canvas/OffscreenCanvas.zig:74-77 convertToBlob() 返回空 Blob

webapi/canvas/OffscreenCanvas.zig:80 transferToImageBitmap() 返回 null

✗ 完全缺失

CSS 布局引擎 盒模型 / 级联 / 继承 Grid / Flex 像素渲染 WebGL getUserMedia WebRTC Web Audio IndexedDB ServiceWorker SVG 渲染 CSS Animations 执行 @media print getComputedStyle (真实值)
语义警告webapi/IntersectionObserver.zig 因为没有布局引擎,所有元素一律视为完全可见。 任何依赖"滚动到视口才触发加载"的脚本(懒加载、无限列表)都会一次性全触发—— 这通常对爬虫来说是好事,因为页面一上来就全展开了,但对 A/B 测试脚本是错误行为。
§ 07 / NETWORK

网络栈 · libcurl 中心化

libcurl + BoringSSL + nghttp2 + brotli + zlib。关掉所有非 HTTP 协议。

libcurl
HTTP/HTTPS/WebSocket(关掉 FTP/IMAP/LDAP)
build.zig:439-600+
TLS
BoringSSL (Google OpenSSL fork)
build.zig:296-437
HTTP/2
nghttp2
build.zig:296-437
压缩
brotli + zlib + zstd
build.zig:296-437
事件循环
CurlM multi handle + epoll/kqueue + wakeup pipe
network/Network.zig:52-150

HTTP 客户端

  • browser/HttpClient.zig
  • 持久连接池(configurable)
  • 并发限流: --http-max-concurrent, --http-max-host-open
  • 超时: 连接 + 传输分开
  • 响应大小上限: --http-max-response-size
  • 最多 10 次重定向

安全相关

  • SSRF 防护: network/IpFilter.zig --block-private-networks 屏蔽 RFC 1918 + IPv6 ULA,在 DNS 解析后执行
  • 自定义 CIDR: --block-cidrs
  • Robots.txt: network/Robots.zig --obey-robots
  • WebBot Auth: network/WebBotAuth.zig Ed25519 签名

缓存

  • network/cache/Cache.zig + FsCache.zig
  • 可选文件系统缓存 --http-cache-dir
  • 尊重 HTTP 缓存头(Cache-Control / ETag / Last-Modified)
  • 按 URL + request headers 为 key

请求拦截

  • cdp/domains/fetch.zig
  • CDP Fetch 域可拦截/改写任何请求
  • Pending transfer 挂在 BrowserContext.intercept_state
  • 客户端可 abort / allow / mock response
§ 08 / MCP

MCP 集成(独特卖点)

Model Context Protocol 服务器,面向 Claude / Cursor / Cline 这类 AI Agent 工具。大部分浏览器只暴露 CDP 或 WebDriver,Lightpanda 把 MCP 当一等公民。

// 启动流
$ lightpanda mcp [--cdp-port 9223]
    ↓
main() 起 mcpThread()                       // main.zig:179-194
    ↓
mcp.Server.init(browser, session, http)     // mcp/Server.zig:27-54
    ↓
mcp.router.processRequests()                // 读 stdin / 写 stdout
    ↓
JSON-RPC 2.0 请求 → 路由 → handler → 响应

协议方法

  • initialize — 握手,返回协议版本
  • ping — 心跳
  • resources/list — 枚举资源
  • resources/read — 读资源

资源(2 种)

  • mcp://page/html — 完整序列化 DOM
  • mcp://page/markdown — token-efficient Markdown
  • mcp/resources.zig:9-22

给 AI Agent 的工具(20+)

类别工具
导航goto(url, timeout, waitUntil), navigate()
提取markdown(url), links(url), semantic_tree(url, maxDepth), interactiveElements(url), structuredData(url), detectForms(url)
交互click(backendNodeId), fill(backendNodeId, text), hover, press(key), scroll(x, y)
检查nodeDetails(backendNodeId) — tag/role/name/interactivity/value/href/checked/options
JSevaluate(script, url, timeout, waitUntil), eval()
等待waitForSelector(selector, timeout)

mcp/tools.zig:48-300+

为什么有价值? 用 Puppeteer 驱动 Chrome 时,AI 要学 CDP 协议 + 自己管 nodeId。 用 Lightpanda MCP,Claude 直接用 click(5) / fill(3, "hello") 这类语义化 tool call, 不需要学协议细节markdown(url) 还把 DOM 降维成 LLM 友好的 token 密集型文本。
§ 09 / BUILD

构建系统

build.zig 34 KB,非平凡。把 V8 / Rust crate / libcurl + 依赖链全串起来。

外部依赖(build.zig.zon)

  • v8 — 预编译或源码(源码 ~1h)
  • curl — 加 boringssl + nghttp2 + brotli + zlib + zstd
  • html5ever — Rust crate,仓内自带

产物

  • lightpanda — 主程序
  • lightpanda-snapshot-creator — V8 snapshot 生成
  • legacy_test — 集成测试 runner

主要步骤

  • build.zig:213-233 V8 链接 + ASAN/TSAN 选项
  • build.zig:235-265 跑 cargo build 编译 html5ever → .aaddObjectFile
  • build.zig:439-600+ 编译 libcurl(HTTP/2 + WebSocket + HTTPS + IPv6,关掉 FTP/IMAP/LDAP)
  • build.zig:296-437 编译 zlib/brotli/nghttp2/BoringSSL

常用命令

zig build             # 编译
zig build test        # 测试
zig build fmt         # 格式化
zig build -Doptimize=ReleaseSafe
§ 10 / LIMITS & TRADE-OFFS

已知限制与架构权衡

从代码里读出来的真实状况,不看营销。TODOs、桩方法、unreachable 都是一手线索。

有意为之的设计取舍

零渲染

Canvas 桩、CSS 解析但不布局、无盒模型、无计算样式。
代价:截图、视觉校验、依赖元素位置的反爬全部失效。
收益:11× 速度来源。

无布局引擎

IntersectionObserver 永远"可见",媒体查询被忽略,@font-face 注册但不加载。
影响:懒加载一次全展开(爬虫友好),但 A/B 脚本可能出错。

单页上下文

cdp/CDP.zig:270-281 BrowserContext 唯一、Session 唯一、Page 唯一活动。
影响:没有真正的多标签并发,需要并发时必须多进程。

Fetch body 常驻内存

cdp/CDP.zig:384 捕获的响应 body 不流到磁盘。
影响:抓巨型文件会内存爆炸,典型页面无忧。

代码里的 TODO 线索

位置TODO / 桩
cdp/domains/page.zigtransitionTypereferrerPolicy 枚举
cdp/domains/dom.zigquads 即使元素应隐藏也照填
cdp/domains/network.zig子 frame 没进 Network.getCertificateDetails
cdp/domains/fetch.zig跨页面请求回复可能跨 context 泄漏
cdp/domains/emulation.zigDevice emulation 是 no-op(本来就没布局)
cdp/domains/browser.zig窗口尺寸硬编码
cdp/AXNode.zigAccessibility tree 在 label_element / label_wrap 有 TODO
webapi/selector/Parser.zig复杂选择器 :has() 等可能桩

最终判断表

场景Lightpanda说明
批量抓静态/半动态页面✓ 强烈推荐比 Chrome 省 9× 内存
SSR 测试✓ 合适DOM 正确性为一等公民
给 AI Agent 当"浏览器臂"✓ 原生支持MCP 一等公民,20+ 语义工具
跑 Playwright/Puppeteer 脚本△ 大部分能跑兼容 CDP,但不支持需要截图/布局的 API
需要截图或像素校验✗ 不行没有渲染管线
重 SPA(依赖可见性懒加载)△ 语义偏差所有元素"可见",懒加载一次全触发
WebRTC / WebGL / ServiceWorker✗ 不行全部未实现
多标签并发✗ 不行单 BrowserContext 约束
公司 SaaS 后端(AGPL)△ 注意自托管可能触发源码披露义务