MCP server · works with any client
Playwright was built to test pages you wrote, where you already know the selectors. An agent is working pages it has never seen — and the snapshot Playwright's MCP server hands it to decide what to click is 241,988 characters for one Wikipedia article. That is so large the server writes it to a file rather than returning it. Here is that page both ways, at the same moment.
Measured, not asserted
The left column is page._snapshotForAI() — the exact string Playwright's MCP
server sends a model, checked against the live server. Each route gets its own browser and
navigation, so neither warms a cache for the other. npm run bench reproduces
this; run it before believing it.
| Page | Playwright MCP | agent-browser | Smaller |
|---|---|---|---|
| en.wikipedia.org/wiki/Accessibility | 241,988 | 3,877 | 62.4× |
| developer.mozilla.org · fetch | 167,599 | 2,667 | 62.8× |
| news.ycombinator.com | 57,447 | 1,740 | 33.0× |
| playwright.dev/docs/intro | 37,296 | 2,905 | 12.8× |
| example.com | 315 | 208 | 1.5× |
| Total · 5 pages | 504,645 | 11,397 | 44.3× |
example.com stays in the set at 1.5×. A page with nothing on it is the floor, and a
benchmark that drops its worst case is advertising. Time was 5.8s against 3.5s across the five
— 1.7×, because every page here reaches network idle quickly; the large time wins are
on pages that never go idle at all, and this set doesn't contain one yet. An earlier run of this
benchmark measured ariaSnapshot() instead and reported 19.9×; that omits the
[ref] annotations MCP adds, and understated the gap by nearly half. Measured 25 Sep 2026.
Why it is smaller, and what else came out of it
[e12] button "Next" (disabled), navigation and footers collapsed to six links and a count.e12 across snapshots for as long as it exists.click checks what is actually on top of the target, presses that layer's Accept or Close, and says which one it moved.wait answers one of three things: there; still loading; or absent on a page that has gone quiet.<select> years agoaria-selected rather than from the click having landed.fill_secret takes a key name from your credentials file. The value reaches the page and not the conversation.console gives its errors and uncaught exceptions; network gives its requests, filtered by failed, third-party or URL. A tracker injected after hydration leaves nothing in the served HTML — this is how you catch it.blocked: this is an interstitial bot check (Cloudflare) — instead of arriving as a page with nothing on it.It does not try to defeat bot protection. A Cloudflare interstitial, a block page or a rate-limit notice is reported, so you know what you are looking at — and the way through is to be a browser you are genuinely signed in to, or to use the site's API.
Dressing up as something else is a race that gets lost on the next update, and it breaks the terms of most sites worth visiting.
The comparison a reader who knows the field will want
44× is large because Playwright's format is verbose, not because this is magic.
browser-use already builds a compact representation and is far more widely adopted:
measured the same day, its own output on these five pages totals 38,101 characters
against 11,397 — about 3.3×. On example.com it beats this tool, 138 to 208,
because a page with almost nothing on it still costs us a header and a URL.
bench/browser-use.py reproduces it. Stagehand is left unmeasured rather than
estimated: v4 expects a paid Browserbase account.
Limits, before they surprise you
AB_BROWSER=firefox switches engine and the whole suite passes on each.js escape hatch.