A browser for agents needs more than one score

A Rust browser engine built for agents, compared with Chrome across 1,024 sites, and why text, links, pixels and scripts each need their own result.

November 20267 min readAgent systemsEvaluationPlatforms

Separate coral, graphite and deep violet tangles occupy different parts of the canvas, with sky grey scumble between them and chalk scrapes.
On this page
  1. Designing for what agents consume
  2. Scoring it against Chrome
  3. Turning scripts on
  4. Choosing the capability you need

In August 2026 Cloudflare published Kitesurf, a headless browser built for AI agents rather than for people looking at screens. Its benchmark over 14 URLs demonstrated a trade: three to seven times less CPU and memory than Chromium, at the price of taking a little under twice as long on the wall clock. The trade makes sense once you look at what an agent consumes, which is text, links, enough structure to act on them and the occasional screenshot that doesn’t have to be beautiful.

I applied that design approach to an engine for Ferrite, my constrained Rust application platform, but I couldn’t use the libraries that made it work for Cloudflare. Kitesurf stands on Blitz, Stylo, blitz-paint and Parley, with Boa to evaluate JavaScript. A Ferrite application gets one dependency, forbids unsafe code, opens no sockets and reaches the network only through an allowlisted handle, and Boa alone would have brought about 40 crates and an unsafe garbage collector. So I kept Kitesurf’s design decisions, left its libraries behind and built the engine on the Rust standard library, with coding agents doing much of the typing under my direction. Building it is only half of the problem, though. The other half is saying accurately what a browser like this is good for, when text, links, layout, scripts and screenshots all fail in different ways and for different reasons.

Designing for what agents consume

The decisions I took from Kitesurf follow from the job. Agents get sent to arbitrary pages, so the parser aims for a best-effort DOM from truncated, hostile or binary input, with stages designed to degrade to an empty box when they can’t cope. A page is stateless, a value computed from one HTML string. And the engine is isolated, meaning it does no I/O at all, so every byte it sees was fetched by the consuming application through that application’s scoped egress handle, and the browser can’t become a way around the network policy.

Staying on the standard library meant the crate had to contain everything else itself: an HTML5 tokenizer and tree constructor, a CSS selector parser and cascade, layout, a software rasterizer, image decoding, a PNG encoder and a Chrome DevTools Protocol facade shaped after obscura, a project that also inspired Kitesurf. That zero-dependency claim stops at the engine’s own crate boundary, since the applications that mount it still depend on the platform. There are two of them, a service that answers requests for text, Markdown, links, titles, serialized HTML and screenshots, and a studio frontend that embeds the same engine.

JavaScript got a decision record of its own, because the obvious candidates all failed in different ways. QuickJS, V8 and SpiderMonkey meant foreign-function bindings. Boa failed on its crates and its garbage collector. Hosting QuickJS in WebAssembly on wasmi broke the crate rule and relied on unsafe code, and that approach would have pushed DOM operations across a module boundary. That left an interpreter inside the engine, about 10,800 lines of Rust when the decision was written down, with defaults of 20 million steps per script and event-loop phase, a 10-second virtual timer horizon and a stack limit. A 20-second wall budget covers the whole page run when the host supplies a monotonic clock. The step budget was sized against real work, since Prism on DevDocs needs about 5.5 million steps on its own; the separate wall budget prevents a page with many individually bounded scripts from running for minutes.

Screenshots are bounded in the same spirit. The canvas stops at 8,192 pixels high by default and at 64 MiB of pixel data in any case, the CDP path compares a projected cost against a budget before it allocates anything, and the PNG encoder streams its rows, because an oversized screenshot could otherwise act as a poison pill for the whole instance. Extraction has no such cap, so an agent reading the bottom of a very long page still gets the text.

Scoring it against Chrome

To measure what all of this amounted to, I had the agents build a corpus of 1,024 sites: eight of Kitesurf’s 14 URLs, the other six being behind bot walls, 135 chosen by hand and 881 from the Tranco ranking. Chrome 148 recorded a reference for each one at 1,280 pixels wide with JavaScript disabled, and the engine was compared with that reference on title, visible text, links, page height and pixels. Every site ended up as pass, warn, fail or uncaptured, and the snapshot from August 24 reads 432 pass, 280 warn, 302 fail and 10 uncaptured.

Read as one number, that’s a browser that passes 42 percent of the corpus, and the number tells you almost nothing. A site passes only if every axis passes, and fails if any axis fails. Here is the same run by axis, across the 1,014 sites that could be compared:

AxisResult
Titleexact on all 1,014
Linksmean score 0.999, with 1 warn and 1 fail
Textmean score 0.945, with 50 warns and 31 fails
Page heightmean 1.197 times Chrome’s, with 60 warns and 27 fails
Pixels259 warns and 269 fails

Pixels dominate. In the first half of the corpus, 144 of 168 failures failed on pixels and nothing else. The rasterizer draws text in an 8×16 bitmap face derived from the old VGA font, ASCII only, but the pixel thresholds had been calibrated to tolerate a font change, so those failures can’t simply be dismissed as bitmap text looking different. The metric measures ink placement as well as luminance; it can expose collapsed tables, displaced content and badly wrapped lines. A screenshot user and a link extractor therefore face very different failure rates. A single verdict collapses those two jobs into one answer and serves neither of them.

The text score measures overlap between word multisets, while the link score compares sets of raw URLs. Their averages are useful summaries, but an agent needs the result for the page it was sent to. The 10 uncaptured sites keep a column of their own rather than quietly leaving the denominator; they include gnu.org and two entries from w3.org, so the run supplies no result for those pages.

Reading individual rows is also what turned the corpus into engineering work rather than a report card. Thirty-four sites link a print stylesheet, and wetter.com came back with no words at all against Chrome’s roughly 1,900 until the engine honored the media attribute on <link>. A glyph advance that was too wide wrapped text about 35 percent early and made pages 1.3 to 2 times too tall; it ended up at 0.45 em, against the 0.415 to 0.489 em measured in Chrome. The engine’s one panic, a UTF-8 mistake in the code that strips comments, turned up and was fixed. Not every gap belonged to the engine either: on ys7.com the difference included 124 hidden links on the capture side.

Turning scripts on

The no-script run is the easy case, because a good deal of the modern web arrives as a script bundle. Running the same 1,024 sites with scripts gives 253 pass, 296 warn, 465 fail and 9 uncaptured, plus hp.com, which left no engine result at all after its 60-script bundle ran into a 120-second limit. Mean text drops to 0.777 and mean links to 0.834. Twenty-seven sites moved up to a pass. But 116 sites went from pass to fail, 71 of them on text. The two runs are dated snapshots, so movement in either direction tells me where to look without telling me why.

The most flattering figure is the script count: the engine found 28,579 scripts and ran 25,714 of them without error, almost exactly 90 percent. jetbrains.com shows what that figure hides. All 14 of its scripts ran cleanly, and the result scored 0 on text and 0 on links, with a page 0.088 times the height of Chrome’s. It isn’t a freak case either: 32 sites scored zero on text with no script errors at all, and 43 of the 195 text failures came from sites whose scripts were clean. flashscore.com goes the other way, with 31 of its 81 scripts failing and the page still scoring 0.423 on text, 0.620 on links and 0.811 on pixels. An error count tells you how much of the code the interpreter got through. It doesn’t tell you whether the agent got the page.

Errors are still useful, because they can be triaged. The error list names platform objects that scripts expected, such as Intl, CSSStyleSheet, ReadableStream and Blob, and working through the second half of the corpus added three globals and three engine fixes. The silent cases are the expensive ones, since nothing points at them except the comparison with Chrome.

Choosing the capability you need

An agent driving it over CDP gets real interaction within limits: a click has to press and release on the same node before it toggles a checkbox, follows a label or records a form submission, and a method the engine doesn’t implement answers with -32601 rather than pretending. It has no logins and no persistent cookies, reaches the network with GET and POST only and doesn’t see response headers, so the supported operations have to match the job the agent is doing.

The no-script run’s median was about 123 milliseconds per site, with a slowest case of nearly 30 seconds. That spread belongs beside the capability scores when choosing which jobs to send to the engine.

That’s a much longer answer than a compatibility percentage, and it’s the one you can plan against, because what someone building on a browser like this needs to know is whether the part of Chrome they depend on is the part that’s there.

Chris Chabot, November 2026