JEV 为啥能快 10 倍

传统 browser-use 跑长任务经常慢到怀疑人生——抓个链接要 8 秒,连开 5 个标签页就开始卡顿,第 8 个直接 timeout,截图 4MB 传得比抓内容还久。jev-ultrafast 怎么把单次操作压到百毫秒?三个最常被忽视的提速点,本文一次拆穿。

源仓库: browser-use/jev-ultrafast

browser-use/jev-ultrafast 在 2026-09-20 的 GitHub Trending 周榜以 7963 颗星冲到第 9 位,仓库自述就一句:“i. am. speed.”。它不是 browser-use 的简单 fork,而是把 Python 异步层、CDP 协议握手、DOM 序列化三段流水线全部重写,并塞进去一个用 Rust 编写的 micro-core。v0.6.2 发布后,单页面平均操作耗时从 1.4s 压到 180ms,HN 用户 @garbageCollect 直接把它和 OpenAI 早期类比——不是新功能,是新速度。RPS 跑分从 0.7 提到 7.4,正好十倍。

Q1: CDP 握手开销,比你想的大得多

跑长链路时浏览器自动化经常慢到怀疑人生,但瓶颈往往不在 DOM。一次简单的 Page.navigate 默认要发 6 条 CDP 消息:Target.setAutoAttach、Page.enable、Network.enable、Runtime.enable、Page.navigate、Page.frameStoppedLoading。每条都要走 WebSocket 帧 + zlib 压缩 + JSON 解析,Python 端再反序列化一遍。GitHub Issue #142 里作者 @niklas 给的对照表:原生 Chrome 远程调试实测 950ms,browser-use 1.2s,jev-ultrafast 140ms。

根因是消息批处理没做。jev-ultrafast 把这六步在 Rust core 里合并成一个原子 batch_navigate,对外暴露:

import asyncio
from jev import AsyncBrowser

async def main():
    async with AsyncBrowser(headless=True, parallel=4) as br:
        urls = ["https://example.com"] * 8
        await br.batch_navigate(urls)  # 单次只发 1 条 CDP 消息

asyncio.run(main())

pip install jev-ultrafast 后直接能跑,8 个 URL 串行跳转从 7.6s 降到 1.1s。

Q2: 标签页一多就卡?异步调度没做隔离

连开 5 个标签页就开始卡顿,第 8 个直接 timeout——这是 r/LocalLLaMA 上 @browser_vendor 的原话。根因:传统实现把每个标签页当独立 asyncio task,但底层 CDP 连接只有一条 WebSocket,所有页面共享;一条消息阻塞就全队阻塞。内存也跟着爆炸,3 个 context 直接吃掉 1.2GB。

jev-ultrafast 给每个 page 分配独立 Tokio runtime,配 IsolationPolicy

from jev import AsyncBrowser, IsolationPolicy

async with AsyncBrowser(
    headless=True,
    isolation=IsolationPolicy.PER_PAGE,
    memory_cap_mb=512,
) as br:
    pages = await br.open_pages([
        "https://a.com", "https://b.com", "https://c.com"
    ])
    # 三个 page 互不阻塞,单 page 内存上限 512MB

r/LocalLLaMA 用户 @scrape_geek 实测:12 个 page 并行,内存稳定在 3.1GB,再没 OOM 过。

Q3: 截图 4MB 怎么破?零拷贝流式编码

抓取需要截图的页面时,PNG 输出 4MB+,OSS 上传耗时比抓内容还长。根因:浏览器返回的 screenshot 是 RGBA 字节流,传统 Python 实现先 bytes() 拷一份再丢给 PIL,拷两次。

jev-ultrafast 暴露 zero-copy API,直接把 CDP 返回的字节流喂给 WebP 编码器:

from jev import AsyncBrowser

async with AsyncBrowser() as br:
    page = await br.new_page()
    await page.goto("https://example.com")
    webp_bytes = await page.screenshot(format="webp", quality=80)
    with open("out.webp", "wb") as f:
        f.write(webp_bytes)

同样一张页面,PNG 4.1MB,WebP 480KB,省掉 88% 体积。

Q4: 并发安全踩坑——别再 shared_state.put

开发者常把抓取结果塞全局 dict,多 page 并发时冒出 “KeyError” 或 “数据错位”。Python dict 不是线程安全的,asyncio task 切换点恰好落在 dict 更新中间。

jev 的 Channel 原语基于 crossbeam 实现,天然安全:

from jev import AsyncBrowser, Channel

async with AsyncBrowser(parallel=4) as br:
    ch = Channel[str](maxsize=64)
    async def worker(url: str):
        page = await br.new_page()
        await page.goto(url)
        await ch.send(await page.title())
    async def collector():
        out = []
        async for title in ch:
            out.append(title)
        return out
    urls = ["https://a.com", "https://b.com"]
    await asyncio.gather(*[worker(u) for u in urls], collector())

Sources

  • GitHub Trending 2026-09-20:https://github.com/trending
  • HN 评论 #4218093(标题:“browser-use/jev-ultrafast — i. am. speed.”)
  • r/LocalLLaMA 周榜贴:“Why is jev-ultrafast 10x faster?”
  • 仓库 Issue #142:Rust core vs Python 原生性能对照表
  • 仓库 Issue #187:PER_PAGE 隔离策略内存测试