Cases / #32 · 2026-10-10 · Easy
A page for an LLM: HTML, innerText or the accessibility snapshot?
Three ways to hand a rendered page to a model — raw HTML, innerText and Playwright's ARIA snapshot — measured on three real pages: how big each is and what it keeps.
Run on production on 2026-10-10: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go
The problem
Agents and extraction prompts need the page as text. Raw HTML is the obvious choice and the most expensive one; innerText is small but throws away links and structure. Playwright can also give you the page as the accessibility tree sees it — roles, names and links in YAML. How big is each on real pages, and what does the model get?
What we used, and why
| What | Why |
|---|---|
page.content() | The serialized DOM — everything, markup included. |
locator("body").innerText() | What a person reads: text only, no links or structure. |
locator("body").ariaSnapshot() | The accessibility tree as YAML: headings, lists, buttons, links with their URLs — the structure a screen reader uses. |
| Three page shapes | Hacker News (a table layout), a Wikipedia article (long prose) and a shop catalogue. |
How it works
- Launch Chromium and open each page (waiting for the HTML).
- Take the HTML, the inner text and the ARIA snapshot of the body.
- Compare their sizes, count the links the snapshot keeps, and print the first lines of each snapshot.
The code
The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:
CDPFLEET_API_KEY— your API key (dashboard → API keys)PROXY_URL— your proxy, e.g.http://user:[email protected]:8000
// npm install [email protected]
// env: CDPFLEET_API_KEY, PROXY_URL
import { chromium } from 'playwright';
const KEY = process.env.CDPFLEET_API_KEY;
const URLS = ['https://news.ycombinator.com/', 'https://en.wikipedia.org/wiki/Web_scraping', 'https://books.toscrape.com/'];
const res = await fetch('https://starter.cdpfleet.com/chromium/session', {
method: 'POST',
headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
body: JSON.stringify({ proxy: process.env.PROXY_URL, headless: 'new' }),
});
if (!res.ok) throw new Error(`launch ${res.status} ${await res.text()}`);
const { wsUrl } = await res.json();
const browser = await chromium.connect(wsUrl, { headers: { 'x-api-key': KEY } });
try {
const page = await browser.newPage();
const pages = [];
for (const url of URLS) {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
const html = await page.content();
const text = await page.locator('body').innerText();
const aria = await page.locator('body').ariaSnapshot();
pages.push({
url,
html_chars: html.length,
text_chars: text.length,
aria_chars: aria.length,
aria_links: (aria.match(/- link /g) || []).length,
aria_vs_html: Math.round((aria.length / html.length) * 1000) / 10,
aria_sample: aria.split('\n').slice(0, 6).join('\n'),
});
}
console.log(JSON.stringify({ pages }, null, 2));
} finally {
await browser.close();
}
# pip install playwright==1.60.0 requests
# env: CDPFLEET_API_KEY, PROXY_URL
import json
import os
import requests
from playwright.sync_api import sync_playwright
KEY = os.environ["CDPFLEET_API_KEY"]
URLS = ["https://news.ycombinator.com/", "https://en.wikipedia.org/wiki/Web_scraping", "https://books.toscrape.com/"]
res = requests.post("https://starter.cdpfleet.com/chromium/session",
headers={"x-api-key": KEY},
json={"proxy": os.environ["PROXY_URL"], "headless": "new"}, timeout=60)
if not res.ok:
raise SystemExit(f"launch {res.status_code} {res.text}")
with sync_playwright() as p:
browser = p.chromium.connect(res.json()["wsUrl"], headers={"x-api-key": KEY})
try:
page = browser.new_page()
pages = []
for url in URLS:
page.goto(url, wait_until="domcontentloaded", timeout=60000)
html = page.content()
text = page.locator("body").inner_text()
aria = page.locator("body").aria_snapshot()
pages.append({
"url": url,
"html_chars": len(html),
"text_chars": len(text),
"aria_chars": len(aria),
"aria_links": aria.count("- link "),
"aria_vs_html": round(len(aria) / len(html) * 100, 1),
"aria_sample": "\n".join(aria.split("\n")[:6]),
})
print(json.dumps({"pages": pages}, indent=2, ensure_ascii=False))
finally:
browser.close()
// Maven: com.microsoft.playwright:playwright:1.60.0, com.google.code.gson:gson:2.11.0
// Run with PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1. env: CDPFLEET_API_KEY, PROXY_URL
import com.google.gson.*;
import com.microsoft.playwright.*;
import com.microsoft.playwright.options.WaitUntilState;
import java.net.URI;
import java.net.http.*;
import java.util.*;
public class Main {
static final String KEY = System.getenv("CDPFLEET_API_KEY");
static final String[] URLS = { "https://news.ycombinator.com/", "https://en.wikipedia.org/wiki/Web_scraping", "https://books.toscrape.com/" };
static final Gson GSON = new GsonBuilder().serializeNulls().create();
public static void main(String[] args) throws Exception {
String body = "{\"proxy\": " + GSON.toJson(System.getenv("PROXY_URL")) + ", \"headless\": \"new\"}";
HttpResponse<String> res = HttpClient.newHttpClient().send(HttpRequest.newBuilder(URI.create("https://starter.cdpfleet.com/chromium/session"))
.header("x-api-key", KEY).header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body)).build(), HttpResponse.BodyHandlers.ofString());
if (res.statusCode() != 200) throw new RuntimeException("launch: " + res.statusCode() + " " + res.body());
String wsUrl = JsonParser.parseString(res.body()).getAsJsonObject().get("wsUrl").getAsString();
try (Playwright playwright = Playwright.create()) {
Browser browser = playwright.chromium().connect(wsUrl, new BrowserType.ConnectOptions().setHeaders(Map.of("x-api-key", KEY)));
try {
Page page = browser.newPage();
JsonArray pages = new JsonArray();
for (String url : URLS) {
page.navigate(url, new Page.NavigateOptions().setWaitUntil(WaitUntilState.DOMCONTENTLOADED).setTimeout(60000));
String html = page.content();
String text = page.locator("body").innerText();
String aria = page.locator("body").ariaSnapshot();
String[] lines = aria.split("\n", -1);
JsonObject p = new JsonObject();
p.addProperty("url", url);
p.addProperty("html_chars", html.length());
p.addProperty("text_chars", text.length());
p.addProperty("aria_chars", aria.length());
p.addProperty("aria_links", aria.split("- link ", -1).length - 1);
p.addProperty("aria_vs_html", Math.round((double) aria.length() / html.length() * 1000) / 10.0);
p.addProperty("aria_sample", String.join("\n", Arrays.copyOfRange(lines, 0, Math.min(6, lines.length))));
pages.add(p);
}
JsonObject out = new JsonObject();
out.add("pages", pages);
System.out.println(new GsonBuilder().serializeNulls().setPrettyPrinting().disableHtmlEscaping().create().toJson(out));
} finally {
browser.close();
}
}
}
}
// dotnet add package Microsoft.Playwright --version 1.60.0
// env: CDPFLEET_API_KEY, PROXY_URL
using System.Net.Http.Json;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Nodes;
using Microsoft.Playwright;
string[] urls = ["https://news.ycombinator.com/", "https://en.wikipedia.org/wiki/Web_scraping", "https://books.toscrape.com/"];
var key = Environment.GetEnvironmentVariable("CDPFLEET_API_KEY")!;
using var http = new HttpClient();
http.DefaultRequestHeaders.Add("x-api-key", key);
var res = await http.PostAsJsonAsync("https://starter.cdpfleet.com/chromium/session",
new { proxy = Environment.GetEnvironmentVariable("PROXY_URL"), headless = "new" });
if (!res.IsSuccessStatusCode) throw new Exception($"launch: {(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");
var wsUrl = (await res.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("wsUrl").GetString()!;
using var playwright = await Playwright.CreateAsync();
var browser = await playwright.Chromium.ConnectAsync(wsUrl, new() { Headers = new Dictionary<string, string> { ["x-api-key"] = key } });
try
{
var page = await browser.NewPageAsync();
var pages = new JsonArray();
foreach (var url in urls)
{
await page.GotoAsync(url, new() { WaitUntil = WaitUntilState.DOMContentLoaded, Timeout = 60000 });
var html = await page.ContentAsync();
var text = await page.Locator("body").InnerTextAsync();
var aria = await page.Locator("body").AriaSnapshotAsync();
pages.Add(new JsonObject
{
["url"] = url,
["html_chars"] = html.Length,
["text_chars"] = text.Length,
["aria_chars"] = aria.Length,
["aria_links"] = aria.Split("- link ").Length - 1,
["aria_vs_html"] = Math.Round((double)aria.Length / html.Length * 1000, MidpointRounding.AwayFromZero) / 10,
["aria_sample"] = string.Join("\n", aria.Split('\n').Take(6)),
});
}
var result = new JsonObject { ["pages"] = pages };
Console.WriteLine(result.ToJsonString(new JsonSerializerOptions { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }));
}
finally
{
await browser.CloseAsync();
}
// go get github.com/playwright-community/[email protected]
// Driver: build playwright-core 1.60.0 from npm and set PLAYWRIGHT_DRIVER_PATH (see /docs/quickstart).
// env: CDPFLEET_API_KEY, PROXY_URL
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"math"
"net/http"
"os"
"strings"
"unicode/utf16"
"github.com/playwright-community/playwright-go"
)
var key = os.Getenv("CDPFLEET_API_KEY")
var urls = []string{"https://news.ycombinator.com/", "https://en.wikipedia.org/wiki/Web_scraping", "https://books.toscrape.com/"}
func launch(name string, options map[string]any) (map[string]any, error) {
body, _ := json.Marshal(options)
req, _ := http.NewRequest("POST", "https://starter.cdpfleet.com/"+name+"/session", bytes.NewReader(body))
req.Header.Set("x-api-key", key)
req.Header.Set("content-type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
msg, _ := io.ReadAll(res.Body)
return nil, fmt.Errorf("launch %s: %s %s", name, res.Status, msg)
}
var session map[string]any
return session, json.NewDecoder(res.Body).Decode(&session)
}
type pageStats struct {
URL string `json:"url"`
HTMLChars int `json:"html_chars"`
TextChars int `json:"text_chars"`
AriaChars int `json:"aria_chars"`
AriaLinks int `json:"aria_links"`
AriaVsHTML float64 `json:"aria_vs_html"`
AriaSample string `json:"aria_sample"`
}
// chars counts UTF-16 code units, like String.length in JavaScript, Java and C#.
func chars(s string) int { return len(utf16.Encode([]rune(s))) }
func main() {
session, err := launch("chromium", map[string]any{"proxy": os.Getenv("PROXY_URL"), "headless": "new"})
if err != nil {
log.Fatal(err)
}
pw, err := playwright.Run(&playwright.RunOptions{SkipInstallBrowsers: true})
if err != nil {
log.Fatal(err)
}
defer pw.Stop()
browser, err := pw.Chromium.Connect(session["wsUrl"].(string), playwright.BrowserTypeConnectOptions{Headers: map[string]string{"x-api-key": key}})
if err != nil {
log.Fatal(err)
}
defer browser.Close()
page, err := browser.NewPage()
if err != nil {
log.Fatal(err)
}
pages := []pageStats{}
for _, url := range urls {
if _, err := page.Goto(url, playwright.PageGotoOptions{WaitUntil: playwright.WaitUntilStateDomcontentloaded, Timeout: playwright.Float(60000)}); err != nil {
log.Fatal(err)
}
html, err := page.Content()
if err != nil {
log.Fatal(err)
}
text, err := page.Locator("body").InnerText()
if err != nil {
log.Fatal(err)
}
aria, err := page.Locator("body").AriaSnapshot()
if err != nil {
log.Fatal(err)
}
lines := strings.Split(aria, "\n")
pages = append(pages, pageStats{
URL: url,
HTMLChars: chars(html),
TextChars: chars(text),
AriaChars: chars(aria),
AriaLinks: strings.Count(aria, "- link "),
AriaVsHTML: math.Round(float64(chars(aria))/float64(chars(html))*1000) / 10,
AriaSample: strings.Join(lines[:min(6, len(lines))], "\n"),
})
}
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
enc.Encode(map[string]any{"pages": pages})
fmt.Print(buf.String())
}
What we got
| Page | HTML chars | innerText chars | ARIA snapshot chars | Snapshot / HTML % | Links in snapshot |
|---|---|---|---|---|---|
| https://news.ycombinator.com/ | 34742 | 4096 | 40476 | 116.5 | 224 |
| https://en.wikipedia.org/wiki/Web_scraping | 256505 | 28589 | 71320 | 27.8 | 383 |
| https://books.toscrape.com/ | 51004 | 2029 | 16662 | 32.7 | 84 |
From the Node.js run on 2026-10-10. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.
Raw output (Node.js)
{
"pages": [
{
"url": "https://news.ycombinator.com/",
"html_chars": 34742,
"text_chars": 4096,
"aria_chars": 40476,
"aria_links": 224,
"aria_vs_html": 116.5,
"aria_sample": "- table:\n - rowgroup:\n - row \"Hacker Newsnew | past | comments | ask | show | jobs | submit login\":\n - cell \"Hacker Newsnew | past | comments | ask | show | jobs | submit login\":\n - table:\n - rowgroup:"
},
{
"url": "https://en.wikipedia.org/wiki/Web_scraping",
"html_chars": 256505,
"text_chars": 28589,
"aria_chars": 71320,
"aria_links": 383,
"aria_vs_html": 27.8,
"aria_sample": "- link \"Jump to content\":\n - /url: \"#bodyContent\"\n- banner:\n - navigation \"Site\":\n - button \"Main menu\"\n - link \"Wikipedia The Free Encyclopedia\":"
},
{
"url": "https://books.toscrape.com/",
"html_chars": 51004,
"text_chars": 2029,
"aria_chars": 16662,
"aria_links": 84,
"aria_vs_html": 32.7,
"aria_sample": "- banner:\n - link \"Books to Scrape\":\n - /url: index.html\n - text: We love being scraped!\n- list:\n - listitem:"
}
]
}Takeaways
- The ARIA snapshot is a third of the HTML on ordinary pages (about 28% on Wikipedia, 33% on the catalogue) while keeping every link with its URL, headings and lists.
- On table layouts it can be bigger than the HTML: Hacker News' nested tables repeat each row's text as row and cell names — 117% of the HTML. Snapshot a smaller locator (the story list) instead of
body. innerTextis the smallest by far (5–12% of the HTML) — fine for summaries, useless when the model has to click or follow a link.- Snapshot the part you need:
page.locator("main").ariaSnapshot()or a list locator cuts tokens more than any format choice.