全 Zig 手写、非 Chromium 分支的无头浏览器。
用 html5ever 解析 HTML,V8 跑 JS,libcurl 跑网络,
暴露 CDP + MCP 双协议。
这份文档基于 commit e6cffae 的实际源码,带文件:行号引用。
11× 是真的——代价是完全砍掉了 CSS 布局、像素渲染、Canvas 绘制。 Lightpanda 只保留 DOM 正确性、JS 兼容性、网络保真度、语义提取四件事。 用它做批量抓取、SSR 测试、AI Agent 驱动非常合适; 用它做需要截图、视觉校验、重 SPA的场景会翻车。 AGPL-3.0 license 对商用 SaaS 也要留意。
一个 main,三种模式:CDP 服务器、单次 fetch、MCP stdio。所有模式共用同一套 Browser 实例。
Lightpanda/1.0 Config.zig:325-339Chrome DevTools Protocol 的 Zig 实现。兼容 Puppeteer/Playwright,但只实现了一个子集。路由用 bit-cast 加速。
@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/domains/target.zig导航、脚本求值、生命周期
cdp/domains/page.zig树查询、quads、节点搜索
cdp/domains/dom.zigJS 求值、属性读取、调用栈
cdp/domains/runtime.zig网络事件、请求/响应捕获
cdp/domains/network.zig请求拦截、响应改写
cdp/domains/fetch.zig鼠标、键盘、触摸
cdp/domains/input.zigConsole 日志
cdp/domains/log.zigCookie、LocalStorage
cdp/domains/storage.zig规则读取(部分)
cdp/domains/css.zig版本、窗口边界(多为桩)
cdp/domains/browser.zig状态(桩)
cdp/domains/inspector.zig安全信息
cdp/domains/security.zig设备模拟(桩——无布局)
cdp/domains/emulation.zigAXNode 可访问性树
cdp/domains/accessibility.zig性能指标
cdp/domains/performance.zigMarkdown dump 等
cdp/domains/lp.zigBrowser / Session / Page 三层结构,加上 Runner 事件循环。
Page.navigate(url)(CDP 或直接 API)Runner.wait() 驱动事件循环直到满足等待条件load / DOMContentLoaded / networkidlePage.navigatedSession 持有两个 Arena:
Session.zig:116-117 代价:无跨页面复用,但换来干净快速的 teardown
直接用 Mozilla 的 html5ever(Rust 写的 HTML5 spec 实现),通过 C FFI 被 Zig 调用。
cargo build 编译 src/html5ever/Cargo.tomlliblitefetch_html5ever.amod.addObjectFile(obj) 静态链入html5ever_parse_document()html5ever_parse_document_with_encoding() — charset awarehtml5ever_parse_fragment() — innerHTMLhtml5ever_streaming_parser_create/feed/finish()// 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-176。
ParsedNode 包装 Node* 加可选 element 数据。
错误通过 Parser.err union 和源码位置归属。
用 V8 跑 JS,通过代码生成为每个 WebAPI 类型自动产出绑定。Zig 对象和 V8 对象通过 internal field 指针互映。
--prebuilt-v8-path 优先,否则从 zig-v8 包从源码编译(~1 小时)browser/js/bridge.zig 是代码生成式绑定,对每个 WebAPI 类型自动产出:
new Element(...))JS 对象在 V8 internal field 存 Zig 指针,identity 稳定:同一个 Zig 对象永远映射到同一个 V8 对象。
| Zig 类型 | V8 类型 |
|---|---|
bool | Boolean |
i32 / u32 / f64 | Number |
[]const u8 | String |
*MyType | Object with internal field = 指针 |
?T | null / Object |
error!T | JS 异常 / 值 |
src/browser/webapi/ 下 217 个 .zig 文件。完整、半实现、缺失三档。
webapi/canvas/OffscreenCanvas.zig:74-77
convertToBlob() 返回空 Blob
webapi/canvas/OffscreenCanvas.zig:80
transferToImageBitmap() 返回 null
libcurl + BoringSSL + nghttp2 + brotli + zlib。关掉所有非 HTTP 协议。
--http-max-concurrent, --http-max-host-open--http-max-response-size--block-private-networks 屏蔽 RFC 1918 + IPv6 ULA,在 DNS 解析后执行--block-cidrs--obey-robotsFsCache.zig--http-cache-dirBrowserContext.intercept_stateModel 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 — 读资源mcp://page/html — 完整序列化 DOMmcp://page/markdown — token-efficient Markdown| 类别 | 工具 |
|---|---|
| 导航 | 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 |
| JS | evaluate(script, url, timeout, waitUntil), eval() |
| 等待 | waitForSelector(selector, timeout) |
mcp/tools.zig:48-300+
click(5) / fill(3, "hello") 这类语义化 tool call,
不需要学协议细节。markdown(url) 还把 DOM 降维成 LLM 友好的 token 密集型文本。
build.zig 34 KB,非平凡。把 V8 / Rust crate / libcurl + 依赖链全串起来。
lightpanda — 主程序lightpanda-snapshot-creator — V8 snapshot 生成legacy_test — 集成测试 runner.a → addObjectFilezig build # 编译
zig build test # 测试
zig build fmt # 格式化
zig build -Doptimize=ReleaseSafe
从代码里读出来的真实状况,不看营销。TODOs、桩方法、unreachable 都是一手线索。
Canvas 桩、CSS 解析但不布局、无盒模型、无计算样式。
代价:截图、视觉校验、依赖元素位置的反爬全部失效。
收益:11× 速度来源。
IntersectionObserver 永远"可见",媒体查询被忽略,@font-face 注册但不加载。
影响:懒加载一次全展开(爬虫友好),但 A/B 脚本可能出错。
cdp/CDP.zig:270-281 BrowserContext 唯一、Session 唯一、Page 唯一活动。
影响:没有真正的多标签并发,需要并发时必须多进程。
cdp/CDP.zig:384 捕获的响应 body 不流到磁盘。
影响:抓巨型文件会内存爆炸,典型页面无忧。
| 位置 | TODO / 桩 |
|---|---|
| cdp/domains/page.zig | 缺 transitionType、referrerPolicy 枚举 |
| cdp/domains/dom.zig | quads 即使元素应隐藏也照填 |
| cdp/domains/network.zig | 子 frame 没进 Network.getCertificateDetails |
| cdp/domains/fetch.zig | 跨页面请求回复可能跨 context 泄漏 |
| cdp/domains/emulation.zig | Device emulation 是 no-op(本来就没布局) |
| cdp/domains/browser.zig | 窗口尺寸硬编码 |
| cdp/AXNode.zig | Accessibility 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) | △ 注意 | 自托管可能触发源码披露义务 |
前面 12 段讲"它是什么、怎么实现"。这一段讲"你该不该用、怎么组合"。技术解析的闭环。
Lightpanda 不是给你用的,是给你的脚本用的。 就像你不会直接打开 MySQL,你写代码去查 MySQL——Lightpanda 也一样,你写代码让它去"读"网页。 你平时上网继续用 Chrome;写脚本批量抓 1000 个页面——用 Lightpanda。
| 你用 Chrome | 脚本用 Lightpanda | |
|---|---|---|
| 谁在操作 | 人(鼠标点击) | 代码(goto/click/eval) |
| 目的 | 看、购物、娱乐 | 提取数据 / 监控 / 自动化 |
| 速度 | 人速 | 每秒几十页 |
| 规模 | 1 个页面 | 1000 个页面 |
| 产出 | 脑子里的记忆 | JSON / CSV / 数据库 |
| 界面 | 有窗口看得到 | 完全无 UI,看不到渲染 |
不索引互联网。你给一个 URL,它只处理这一个页面。想搜索得先有 URL 列表。
默认不存任何文件,结果流到 stdout。要留下必须自己重定向到文件或写数据库。
它是"批量读网页 → 提取结构化数据"。保存的不是 HTML 本身,是从 HTML 里挖出来的字段。
| 场景 | 能不能用 | 备注 |
|---|---|---|
| 新闻 / 博客 / Wiki | ✅ 完美 | 纯文字,token 友好 Markdown |
| API 文档、技术站 | ✅ 完美 | 结构化强 |
| 论坛(HN / Reddit / V2EX) | ✅ 完美 | 纯文字内容 |
| SPA 后台管理系统 | ✅ 大多行 | DOM 驱动,需要配合登录态 |
| 企业官网 / SaaS 介绍页 | 🟡 部分 | 标题在文字,卖点常在海报 |
| 电商商品详情页 | ❌ 瞎 | 规格、材质、使用说明全在长图 |
| 小红书 / Instagram | ❌ 瞎 | 核心信息载体是图文笔记 |
| PDF-as-webpage | ❌ 瞎 | 很多文档站把 PDF 转成图嵌入 |
| 视频站 | ❌ | 视频内容根本不是 DOM |
<img src="xxx.jpg"> 的 URL,但不下载图片字节、不做 OCR、不做视觉理解。
这是设计取舍——为了 11× 速度砍掉的能力,不是 bug。
单 Lightpanda 覆盖 60% 的场景,剩下 40%(中文电商、小红书、海报站)必须靠视觉大模型(VLM)补。 分层调度后,总成本比 "真 Chrome + 整页截图 + VLM" 便宜 5-10 倍。
<img src> URL,你精确挑要看的图交给 VLM。
ssh root@VPS '/opt/lightpanda/lightpanda fetch \
--dump markdown \
--wait-until networkidle \
--wait-ms 3000 \
https://目标站.com'
适合:一次性抓一个 URL,把结果直接喂给 LLM 做摘要/问答
ssh root@VPS '/opt/lightpanda/lightpanda fetch \
--dump html \
--wait-until networkidle \
--wait-ms 5000 \
--with-base \
https://目标站.com'
适合:要图片 URL 列表、要页面所有内容。注意:--dump markdown 会省略 hero/图片区块,
想拿到产品主推文案必须用 html 自己 parse。
# 本机开 SSH 隧道
ssh -N -L 9222:127.0.0.1:9222 root@VPS
# 另一个终端跑 Python(Playwright connectOverCDP)
browser = await p.chromium.connect_over_cdp("http://127.0.0.1:9222")
context = await browser.new_context()
await context.add_cookies(cookies) # 从本机 Chrome 导的登录态
page = await context.new_page()
await page.goto("https://后台.com/订单")
data = await page.eval_on_selector_all("...", "...")
适合:需要保持登录态的后台管理系统、多步表单交互、抓取分页列表
默认 --wait-until=done,SPA 内容可能还没渲染就 dump 了。
解法:重页面加 --wait-until networkidle --wait-ms 3000。
Apple 首页就是这样才拿到 hero 主推产品。
--dump markdown 是"语义精简"模式,会跳过图片为主的 hero 区块、产品卡片。
解法:想拿到所有内容用 --dump html,自己写 parser 提取 H2/H3/class。
默认 UA 是 Lightpanda/1.0,很容易被反爬识别封 IP。
解法:加 --user-agent-suffix 或自建 UA。注意代码禁止含 "Mozilla" 的伪装 UA。