介绍 Kitesurf:以代理为先的浏览器,在 Cloudflare Workers 上的 V8 隔离环境中运行

我们是否应该构建自己的浏览器?这是多年来 Cloudflare 内部每隔几个月就会提出的问题之一。不出所料,这类问题会引发长篇讨论,包含多种理由和说服力强的论点,说明我们为什么应该这样做。浏览器显然是我们每天在电脑上使用的最重要的软件;它可以说是互联网的操作系统。我们是一家致力于 构建更美好互联网的公司——谁不想接受构建新浏览器的挑战呢?
但我们从未在这样一项事业的技术难度和我们通过它解决的独特问题之间找到平衡。因此,这个想法一次又一次地被搁置。直到现在。
神奇的事情发生了:我们达到了一个临界点,我们 开发者平台中的一系列强大技术进步成为现实,而 AI 代理的出现和对新型浏览器的需求也同时变得至关重要。
在 Workers 中运行 WebAssembly (Wasm)现在已经非常成熟。像
动态 Workers,
基于 SQLite 的 Durable Objects,
Worker 到 Worker 的 RPC, 更高级的
服务绑定和更高级的
NodeJS 兼容性为更宏大、更复杂的应用打开了大门,这些应用以前根本不可能实现。
限制 Browser Run,我们的无头浏览器自动化 API 产品,随着 AI 的兴起而经历了巨大的增长。代理需要浏览器来执行许多任务,在许多情况下,没有浏览器它们就无法成功。
但有一个问题——像 Chromium 这样的浏览器引擎是为人类构建的,而不是为代理构建的,它们带有 AI 模型根本不需要的开销。它们消耗如此多的内存和计算资源,以至于为每个代理提供自己的实例成本高得令人望而却步,这限制了 Web 的大部分内容只能由最复杂、最昂贵的、具有更高参数知识的 AI 模型访问,同时将许多其他代理应用拒之门外。
我们应该给所有代理一个在 AI 模型重要方面表现出色的浏览器,即使这意味着在仅对人类有用的方面要轻量。例如:
- AI 不关心标签页、主题、浏览器扩展或跨设备同步。它关心的是 token 数量、上下文窗口、可扩展性、性能和成本。
- 结构化、机器可读的内容很重要,但视觉完美、流畅的 60fps 滚动并不重要。如果 CSS 解析稍有偏差或渲染不是像素完美的,代理也不会介意。
- 在 AI 使用浏览器的背景下,威胁模型是不同的。像提示注入和工具安全这样的新问题是首要任务。
面对这些认识,12周前我们再次提出了这个问题:我们应该构建自己的浏览器吗?这一次,答案是一致的:是的!
今天,我们宣布Kitesurf,一个全新的浏览器,它完全运行在Workers之上,是我们专门为代理构建的,在测试版期间可免费使用,可在 Browser Run中获取。
对于常见的代理任务,如截图和HTML提取,Kitesurf在CPU和内存消耗方面比Chromium高效得多。以下是我们如何构建它的故事。系好安全带,这将变得技术性——但我们保证会保持有趣。
它是如何开始的
Kitesurf的起步方式与Cloudflare的许多其他伟大想法一样。有人发现了有趣的东西,然后不知不觉中,他们最终用看似不可能但非常有吸引力的想法“极客狙击”了团队的其他成员。
我们最初从 obscura获得灵感,这是一个用Rust编写的无头引擎,用于AI自动化,具有“无Chrome、无Node.js、无依赖”的特点。

然后,在AI代理的帮助下,我们尝试将其移植到Workers。起初效果并不好。但一旦我们给AI一个可靠的计划和明确的成功定义——足够详细,让代理能够无限循环并在需要时提问——它就成功了。
被这个(勉强)工作的概念验证所震撼,我们决定让团队继续努力。
设计决策
以下是我们开始之前做出的一些设计决策。
测试,测试,再测试
我们知道,从原型转向一个功能齐全的浏览器,能够在生产中大规模地用于任务,需要大量的工作和迭代。我们不否认使用AI加速这一过程是关键。但如何在这样一个复杂的项目中使用AI,同时保持代码和结果的质量,而不失去速度?答案是提供尽可能多的测试。
引入 Web Platform Tests(WPT),这是理想的设置:一套广泛的成功标准,为AI代理提供了评估功能一致性的明确目标。我们策划了分配给代理的功能选择和顺序,让人类专注于架构工作和审查代理的方法。
然而,WPT测试只能到此为止:它们衡量对 W3C标准的符合性,而不是浏览器渲染和与真实网站交互的能力。为了弥合这一差距,我们实现了
集成测试和视觉回归测试的组合——它在真实网站上运行多步骤
测试,不仅通过比较断言,而且通过渲染每一步的输出,以突出任何不希望的差异,来对比Chromium和Kitesurf。
Puppeteer尽可能使用Rust
Cloudflare一直在努力为Workers中的 WebAssembly (Wasm)提供出色的支持。这很棒,因为我们可以使用高性能的C、C++和Rust包,并将它们编译为Wasm。如果我们使用Emscripten(例如)及其多层模拟依赖,编译后的二进制文件可能会变得庞大且缓慢。
相反,我们尽可能选择原生 Rust,并使用 wasm-bindgen 直接编译为 WebAssembly,从而避免不必要的模拟层,并尽可能接近底层运行 .
可靠地异常处理
浏览器必须渲染整个不可靠且有时充满敌意的网络,而不会丢弃其持有的页面,因此异常处理不仅仅是卫生问题——它是应用程序在遇到不良输入时不会直接崩溃而存活下来的方式。
因此,我们预先承诺了一条规则:任何失败都会降级为空白帧或缺失元素,而绝不会导致会话死亡。在每个边界捕获故障,默认使用安全且空的内容,并记录足够的日志以便诊断。
隔离
与在笔记本电脑上运行浏览器(你访问的是信任的网站,并且它们之间共享一些资源是可以接受的)不同,代理指向任务所需的任何内容:来自任意来源的任意代码。
因此,我们构建这个浏览器时假设每次页面加载都是不受信任的输入,每个会话都是全新开始的。每个组件都是隔离的,并且只能访问其功能严格必需的资源。

这似乎非常适合 Cloudflare Workers,其安全模型围绕设计隔离而构建。但平台只为我们提供了隔离区之间的边界。我们仍然需要在应用程序级别执行相同的原则,决定每个组件允许接触什么,并确保没有任何东西泄漏到它不应该泄漏的页面。
尽可能无状态
状态是使失败代价高昂的原因——如果无需重建任何东西,从崩溃中恢复就只是启动一个新实例并重放请求。无状态组件本质上是可丢弃且可并行的:一旦停滞就杀死它,同时运行一千个,并根据需求调整大小,而不是保持热状态。这非常适合自动化,负载突发到达,你能做的最便宜的事情就是启动只消耗其使用量的工作,并在完成后消失。简而言之,只要组件可以无状态,就应该无状态。
我们如何构建
有了良好的计划、广泛的测试和良好的工具环境,我们准备在初始概念验证之外开始。这是 Kitesurf 的请求生命周期的高级视图,至今仍然适用:

让我们深入了解使 Kitesurf 工作的三个主要组件:Engine、PageScript 和 PageRenderer。
从源获取
为了渲染不受信任的网页,浏览器必须从互联网获取任意资源——图像、字体、CSS、JavaScript 和 Wasm 文件。这是浏览器能做的最危险的操作之一。
Kitesurf 通过一个单一组件 SandboxOutbound worker 来完成此操作,其他任何东西都不能直接接触网络——由 Dynamic Workers 强制执行。Engine 使用它来引导页面,获取主文档及其脚本,而 PageScript 获取其他所有内容:样式表、图像、字体以及页面自身的 fetch() 调用。
我们使用 SandboxOutbound 来强制实施 CORS,注入浏览器形状的请求头,过滤响应,并将每个页面的 cookie 保存在各自的 jar 中。任何不符合我们策略的内容都会收到 403——每个组件都精确地获得它所需的网络,仅此而已。

引擎
引擎是 Kitesurf 唯一面向公众的组件。它处理 Chrome DevTools 协议(CDP)WebSocket 和 HTTP REST API,提供一个用于内部测试目的的着陆页,最重要的是,存储每个会话状态。所有其他组件都是无状态的。

使用 CDP 的优势在于客户端兼容性:Puppeteer、Playwright、chrome-remote-interface 以及实际的 Chrome DevTools 前端。将它们指向 Kitesurf,它们都能正常工作。这也是 Browser Run
__的工作原理__与名称所暗示的相反,引擎实际上是 Kitesurf 组件中最简单的。有趣的部分接下来才登场。
PageScript
PageScript 很好地展示了我们新 Workers 特性的强大之处:在这里,就是动态 Workers。在此之前,Kitesurf 根本不可能实现。
以下是 PageScript 内部工作原理的简化示意图。

每个下一页或进程外 iframe(OOPIF)都使用动态 Workers 来启动一个长期存在的 PageScript 隔离环境,该环境处理页面会话,包括一个干净的
globalThis文档对象。
__DOM__然后,DOM 对象被填充解析 HTML 文档和运行所有 JavaScript 脚本的结果。对于解析 HTML 和 CSS,我们使用 Blitz 的一部分,这是一个模块化渲染引擎,以及
,Firefox 的高性能 CSS 解析器,两者都用 Rust 编写。
__Stylo__对于每个找到的
