Kitesurf vs headless Chrome · one day of tests
We spent a day racing Kitesurf, a new browser engine, against headless Chrome. It opened sessions in under five milliseconds and took screenshots ten times faster. It also said yes to everything we asked of it, including things it can't do.
01 · Off the beach
Kitesurf speaks the same protocol as Chrome, the one Puppeteer uses to drive a browser. But it doesn't launch anything. It runs as one long-lived engine, and a new session is a WebSocket connection.
So the first race wasn't close.
That's the fair comparison for a service that keeps an engine running, and an unfair one for a single cold job.
02 · The shutter
Chrome takes 33 to 66 ms per repeated screenshot on any page. It seems to wait for a frame. Kitesurf takes 3 to 8 ms. A clip of one element takes under a millisecond.
Here is one real second of repeated viewport screenshots on the same page.
03 · The wind drops
The time Kitesurf saves on starting and shooting, it spends on navigation. Loading a small local page takes Chrome 8 ms. It takes Kitesurf 174. Even an empty page costs about 110 ms, and going back reloads the page.
On real sites, plain pages cost about the same. Pages heavy with script cost two to five times more, and the numbers vary a lot from run to run.
BBC showed Kitesurf a consent wall and Chrome the news. On Stripe's docs Chrome never reached load, so 45 s is our cut-off, not its time.
04 · Yes to everything
In 30 of Kitesurf's protocol domains, a call to a method it doesn't know doesn't fail. Kitesurf answers with an empty object, which is also what many real successes return. The call returns and nothing happens.
Try it. The buttons on the left replay answers from our probe. If you type your own method, you get what the probe found that domain does.
We sent 115 methods that matter for recording, with sensible arguments, and then checked whether each one did what it said. Score it the usual way, by whether the call returned without an error, and Kitesurf looks nearly perfect.
On the same probe, Chrome got 52 right and none wrong. It returned 9 errors, and 8 of those were for methods only Kitesurf has.
05 · Quiet failures
A recorder must capture what the page did, what went over the network and what the user saw. Kitesurf missed each of the things below, and none of them raised an error.
alert() never reaches Puppeteer's dialog event; the page carries on as if someone clicked OK. With confirm and prompt, the page's own handler never finishes.
fullPage: true on an 8,080 px page returns the 800 px viewport. No error. Ask for 2× pixels and you get 1×.
Every response carries a timing object. On all 82 of them, it holds placeholders. A HAR file from Kitesurf has no usable timings.
Kitesurf reported all 86 requests, with none missed and none invented. But every binary body comes back empty: a PNG, a JPEG and a 200 kB download. It recorded no POST body at all.
console.log, warn and error arrive. A thrown error that nobody catches, the one you most want in a recording, never does.
Tracing.start answers. No data comes. Tracing.end never completes. Network throttling makes the next page load hang until it times out.
06 · Same page, other type
On our own test pages, Kitesurf's screenshots differ from Chrome's by 1 to 5 % of pixels. Most of that is type. Kitesurf ships only two fonts, Inter and Noto Sans, so its text comes out a little bigger and a little lower. Drag the line to compare, switch to Diff to see every pixel that changed, and pick another pair below.
Script app
ChromeKitesurf
These are 14 of the 66 screenshot pairs, all from our own test pages: five pages on their own, then steps of the scripted flow. pixelmatch counted the differing pixels at threshold 0.1, on the same 1280 × 800 viewport. Under 1 % looks like the same page, and most of the rest is type.
07 · Where the day went
Morning
The project is young and changes fast, so everything here is true of one commit, 4a29038.
2 minutes in
"Can't find crate for zerofrom_derive." The crate was there. Running the compiler by hand on the built file showed the real error, from the macOS loader: mis-aligned LINKEDIT string pool.
Narrowing
Two compiler flags cause it. One strips debug info, the other sets a minimum macOS version. Each is harmless alone, but together they make a library that macOS 27's loader refuses. Bazel adds one flag and the Rust rules add the other.
The fix
common --host_compilation_mode=fastbuild in a local Bazel file. Build tools skip the strip, the engine builds, /health says OK.
Afternoon
Reading the test server's log took 4.5 s on Chrome and timed out on Kitesurf. It looked like an engine problem. The cause was Node's built-in fetch on this Mac, which stalled for up to 25 s when it reused an idle connection. A plain HTTP client with no keep-alive brought it to 1 ms.
When a number looks bad, measure the test setup before you blame the engine.
Evening
A smoke test said 114 of 115 methods worked. Checking what each one did said 38. From then on, we tested each feature by checking its effect, not its answer.
Night
The fixture site logs every request it serves, so we know what each engine should have reported. Then we wrote the report.
08 · Verdict
wrangler dev. Here it used 2.6× Chrome's CPU. Cloudflare's own figures for the hosted engine show about 2× less CPU and 6× less memory than Chrome (kitesurf.dev/benchmarks).