Blog
Replace Playwright in Your Agent Pipeline in 5 Minutes
Migrate from Playwright to Unbrowse in 5 minutes. Before/after code comparisons, performance benchmarks (3.6x faster), and a step-by-step migration guide for AI agent pipelines.
Replace Playwright in Your Agent Pipeline in 5 Minutes
If your AI agent uses Playwright to interact with websites, you are paying a tax on every operation: 3-8 seconds per page load, 200+ MB of RAM for each browser instance, and constant maintenance as sites change their HTML. Unbrowse eliminates all of this by calling the APIs that websites use internally, skipping the browser entirely for most operations.
The migration takes 5 minutes. The performance improvement is 3.6x on average. Here is exactly how to do it.
The Performance Gap
We benchmarked Unbrowse against Playwright across 50 common agent tasks — search, data extraction, form submission, price checks, content retrieval. The results, published in our research paper on arXiv:
| Metric | Playwright | Unbrowse | Improvement |
|---|---|---|---|
| Average task completion | 6.2 seconds | 1.7 seconds | 3.6x faster |
| Memory per task | 220 MB | 15 MB | 14.7x less |
| Success rate (no maintenance) | 73% after 30 days | 97% after 30 days | Stable |
| Lines of code per task | 25-50 | 3-10 | 5-8x less |
| Cold start | 1.2 seconds | ~3 ms (Kuri) | 400x faster |
The speed difference comes from skipping the browser rendering pipeline entirely. Playwright launches Chromium, loads HTML, executes JavaScript, renders the page, then extracts data from the DOM. Unbrowse calls the underlying API endpoint directly and gets structured JSON back.
Before and After: Search
Playwright:
import { chromium } from "playwright";
async function searchPlaywright(query) {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(`https://www.google.com/search?q=${encodeURIComponent(query)}`);
await page.waitForSelector("#search");
const results = await page.$$eval(".g", (elements) =>
elements.map((el) => ({
title: el.querySelector("h3")?.textContent || "",
url: el.querySelector("a")?.href || "",
snippet: el.querySelector(".VwiC3b")?.textContent || "",
}))
);
await browser.close();
return results;
}
Unbrowse:
import { Unbrowse } from "@unbrowse/sdk";
const unbrowse = new Unbrowse();
async function searchUnbrowse(query) {
const result = await unbrowse.resolve({
intent: `search for ${query}`,
url: `https://www.google.com/search?q=${encodeURIComponent(query)}`,
});
return result.data;
}
22 lines down to 5. No browser launch. No CSS selectors. No cleanup.
Before and After: Data Extraction
Playwright:
async function getProductPlaywright(url) {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto(url);
await page.waitForSelector("[data-testid='product-title']");
const product = {
title: await page.$eval("[data-testid='product-title']", (el) => el.textContent),
price: await page.$eval(".price-current", (el) => parseFloat(el.textContent.replace(/[^0-9.]/g, ""))),
rating: await page.$eval(".star-rating", (el) => parseFloat(el.getAttribute("data-rating") || "0")),
inStock: await page.$eval(".availability", (el) => el.textContent.includes("In Stock")),
images: await page.$$eval(".product-image img", (imgs) => imgs.map((i) => i.src)),
};
await browser.close();
return product;
}
Unbrowse:
async function getProductUnbrowse(url) {
const result = await unbrowse.resolve({
intent: "get product details including price, rating, and availability",
url,
});
return result.data;
}
The Playwright version uses 5 different CSS selectors, any of which can break when the site updates. The Unbrowse version uses zero selectors because it calls the product API directly. The shadow API returns all product fields as structured JSON — often including data not visible on the page.
Before and After: Multi-Page Workflow
Playwright:
async function compareProductsPlaywright(urls) {
const browser = await chromium.launch();
const results = [];
for (const url of urls) {
const page = await browser.newPage();
await page.goto(url);
await page.waitForLoadState("networkidle");
const data = await page.evaluate(() => {
// Site-specific extraction logic
return {
title: document.querySelector("h1")?.textContent?.trim(),
price: document.querySelector(".price")?.textContent?.trim(),
};
});
results.push(data);
await page.close();
}
await browser.close();
return results;
}
Unbrowse:
async function compareProductsUnbrowse(urls) {
return Promise.all(
urls.map((url) =>
unbrowse.resolve({ intent: "get product name and price", url }).then((r) => r.data)
)
);
}
The Playwright version processes pages sequentially (each page needs its own browser tab). The Unbrowse version runs all requests in parallel because there is no browser to share. For 10 products, Playwright takes 30-80 seconds. Unbrowse takes 1-2 seconds.
Step-by-Step Migration Guide
Step 1: Install Unbrowse (30 seconds)
git clone --single-branch --depth 1 https://github.com/unbrowse-ai/unbrowse.git ~/unbrowse
cd ~/unbrowse && ./setup --host off
Or for SDK only:
npm install @unbrowse/sdk
Step 2: Replace Browser Launch With SDK Init (30 seconds)
Find every instance of:
import { chromium } from "playwright";
const browser = await chromium.launch();
const page = await browser.newPage();
Replace with:
import { Unbrowse } from "@unbrowse/sdk";
const unbrowse = new Unbrowse();
Step 3: Replace page.goto + Selectors With resolve (2 minutes)
For each Playwright interaction:
// Before
await page.goto(url);
await page.waitForSelector(".some-class");
const data = await page.$eval(".some-class", el => el.textContent);
Replace with:
// After
const result = await unbrowse.resolve({ intent: "describe what data you need", url });
const data = result.data;
The key insight: instead of telling Unbrowse which CSS selector to use, you tell it what data you want. Unbrowse figures out which API endpoint has that data.
Step 4: Remove Browser Cleanup (30 seconds)
Delete all instances of:
await page.close();
await browser.close();
Unbrowse has no browser instances to clean up.
Step 5: Parallelize Sequential Operations (1 minute)
Playwright forces sequential page loads because browser tabs share resources. With Unbrowse, convert sequential loops to parallel:
// Before (sequential)
for (const url of urls) {
await page.goto(url);
// ...
}
// After (parallel)
const results = await Promise.all(
urls.map((url) => unbrowse.resolve({ intent: "...", url }))
);
When You Still Need a Browser
Unbrowse replaces Playwright for 90% of agent tasks, but some operations genuinely need a browser:
- Visual testing: Screenshots, visual regression testing
- Complex multi-step forms: OAuth flows with redirects
- Sites with zero API surface: Some legacy sites truly render everything server-side
For these cases, Unbrowse has a built-in browser powered by Kuri (a 464KB Zig-native CDP broker). It launches in ~3ms versus Playwright's 1.2 seconds and includes stealth extensions to avoid bot detection. You get a browser when you need one, but you only pay the cost when you actually need it.
MCP Server Integration
If your agent pipeline uses MCP (Model Context Protocol), Unbrowse drops in as an MCP server:
cd ~/unbrowse && ./setup --host mcp
This writes a config file that any MCP-compatible agent host can import. Your agent calls unbrowse_resolve instead of managing browser instances through MCP.
The Marketplace Accelerator
Every resolve() call that discovers a new shadow API publishes the route to the Unbrowse marketplace. This means:
- Routes discovered by your agent pipeline benefit every other agent
- Routes discovered by other agents benefit your pipeline
- Common sites (Google, Amazon, GitHub, etc.) already have cached routes from other users
- You earn x402 micropayments when others use routes you discovered
The more agents that use Unbrowse, the faster every individual agent gets. This is the network effect that Playwright will never have — it is a local tool, while Unbrowse is shared infrastructure.
Migration Checklist
- Install Unbrowse SDK:
npm install @unbrowse/sdk - Replace
chromium.launch()withnew Unbrowse() - Replace
page.goto()+ selectors withunbrowse.resolve() - Remove
browser.close()/page.close()calls - Convert sequential page loads to
Promise.all() - Remove Playwright from dependencies:
npm uninstall playwright - Delete browser binary cache:
npx playwright uninstall
Total migration time: 5 minutes for a typical agent pipeline. Performance improvement: 3.6x average, up to 30x for parallel workloads. Maintenance reduction: near-zero, because APIs do not break like CSS selectors.
Stop paying the browser tax. Your agent deserves the fast path.