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.

Lewis Tham
April 3, 2026

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() with new Unbrowse()
  • Replace page.goto() + selectors with unbrowse.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.