Kitesurf vs headless Chrome · one day of tests

The browser that said yes.

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.

3engines 10repeats each 115protocol calls 86requests traced
↓ scroll

01 · Off the beach

Chrome starts a process. Kitesurf is already in the water.

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.

Open a session · median of 10 · shown 10× slower
Chromenew headless
0 ms
headless-shellChrome, stripped
0 ms
KitesurfWebSocket connect
ready.
0 ms

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

Then it took screenshots ten times faster.

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.

Repeated viewport PNG · 1 second, real time
Chrome · 66 ms each0
Kitesurf · 6 ms each0

03 · The wind drops

Every page you open costs 150 ms.

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.

Median ms · small local pages

Open a session

Chrome
348
Kitesurf
4.7

Warm goto

Chrome
8
Kitesurf
174

Open, page, goto, close

Chrome
485
headless-shell
142
Kitesurf
178

goBack

Chrome
4
Kitesurf
172

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.

Real sites · one visit each · seconds to load, log scale
ChromeKitesurf

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

Ask it anything. It says {}.

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.

// kitesurf, protocol 1.3 · pick a method

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.

38 / 115 did what they said
38 checked, right 12 answered, wrong 56 answered {}, can't tell 8 Kitesurf's own extras 1 skipped

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

Nothing crashed. Things didn't happen.

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.

The alert nobody saw

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.

Chrome
Kitesurf

The full page that wasn't

fullPage: true on an 8,080 px page returns the 800 px viewport. No error. Ask for 2× pixels and you get 1×.

asked
8080
got
800

Placeholder timings

Every response carries a timing object. On all 82 of them, it holds placeholders. A HAR file from Kitesurf has no usable timings.

timing: { requestTime: 0.017, dnsStart: -1, connectStart: -1, sslStart: -1, sendStart: 0, receiveHeadersEnd: 0 }

Bodies that came back empty

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.

requests seen86/86
bodies right27/39
POST bodies0/3
real timings0/82

Errors that stayed inside

console.log, warn and error arrive. A thrown error that nobody catches, the one you most want in a recording, never does.

console.error("x") → arrived throw new Error() → silence

A trace that never ended

Tracing.start answers. No data comes. Tracing.end never completes. Network throttling makes the next page load hang until it times out.

Tracing.start ← {} Tracing.end ← …

06 · Same page, other type

It draws pages well. Just not quite like Chrome.

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

Kitesurf's screenshot Chrome's screenshot Pixels that differ, marked in red 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

Two traps. Only one was Kitesurf's.

  1. Morning

    Clone it, one day after its open-source release.

    The project is young and changes fast, so everything here is true of one commit, 4a29038.

  2. 2 minutes in

    The build fails with a misleading error.

    "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.

  3. Narrowing

    A five-line proc macro narrows it down.

    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.

  4. The fix

    One line, and a six-minute build.

    common --host_compilation_mode=fastbuild in a local Bazel file. Build tools skip the strip, the engine builds, /health says OK.

  5. Afternoon

    Our own trap, in the test client.

    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.

  6. Evening

    Kitesurf's trap: {}.

    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.

  7. Night

    Two full runs, three engines, ten repeats.

    The fixture site logs every request it serves, so we know what each engine should have reported. Then we wrote the report.

08 · Verdict

A fast camera. Not yet a faithful witness.

Fits now

  • Many short, cheap, read-only jobs on plain pages
  • Viewport screenshots, text and DOM extraction, link checks
  • Jobs where launching a browser is the main cost. Eight sessions open in 13 ms and share one engine
  • Request blocking, mocks and header rewrites all work

Not yet

  • A faithful recording: timings, binary and POST bodies, dialogs, thrown errors, full-page images
  • Anything that must look exactly like Chrome
  • Script-heavy sites, at 2–5× Chrome's load time
  • Trusting a call because it returned

Read with care

  • We ran the engine locally with 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).
  • One commit, one day after release. The gaps above are the kind upstream says it is working on.
  • The Mac wasn't idle, and 10 repeats isn't many. Trust ratios of 2× and more, not 10 % differences.