Files
gh-jezweb-claude-skills-ski…/references/puppeteer-vs-playwright.md
2025-11-30 08:24:08 +08:00

15 KiB

Puppeteer vs Playwright Comparison

Complete comparison guide for choosing between @cloudflare/puppeteer and @cloudflare/playwright.


Quick Recommendation

Use Puppeteer if:

  • Starting a new project
  • Need session management features
  • Want to optimize performance/costs
  • Building screenshot/PDF services
  • Web scraping workflows

Use Playwright if:

  • Already have Playwright tests to migrate
  • Prefer auto-waiting behavior
  • Don't need advanced session features
  • Want cross-browser APIs (even if only Chromium supported now)

Bottom Line: Puppeteer is recommended for most Browser Rendering use cases.


Package Installation

Puppeteer

npm install @cloudflare/puppeteer

Version: 1.0.4 (based on Puppeteer v23.x)

Playwright

npm install @cloudflare/playwright

Version: 1.0.0 (based on Playwright v1.55.0)


API Comparison

Launching a Browser

Puppeteer:

import puppeteer from "@cloudflare/puppeteer";

const browser = await puppeteer.launch(env.MYBROWSER);

Playwright:

import { chromium } from "@cloudflare/playwright";

const browser = await chromium.launch(env.BROWSER);

Key Difference: Playwright uses chromium.launch() (browser-specific), Puppeteer uses puppeteer.launch() (generic).


Basic Screenshot Example

Puppeteer:

import puppeteer from "@cloudflare/puppeteer";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const browser = await puppeteer.launch(env.MYBROWSER);
    const page = await browser.newPage();
    await page.goto("https://example.com");
    const screenshot = await page.screenshot();
    await browser.close();

    return new Response(screenshot, {
      headers: { "content-type": "image/png" }
    });
  }
};

Playwright:

import { chromium } from "@cloudflare/playwright";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const browser = await chromium.launch(env.BROWSER);
    const page = await browser.newPage();
    await page.goto("https://example.com");
    const screenshot = await page.screenshot();
    await browser.close();

    return new Response(screenshot, {
      headers: { "content-type": "image/png" }
    });
  }
};

Key Difference: Nearly identical! Main difference is import and launch method.


Feature Comparison

Feature Puppeteer Playwright Notes
Basic Screenshots Yes Yes Both support PNG/JPEG
PDF Generation Yes Yes Identical API
Page Navigation Yes Yes Similar API
Element Selectors CSS only CSS, text Playwright has more selector types
Auto-waiting Manual Built-in Playwright waits for elements automatically
Session Management Advanced ⚠️ Basic Puppeteer has .sessions(), .history(), .limits()
Session Reuse Yes ⚠️ Limited Puppeteer has .connect() with sessionId
Browser Contexts Yes Yes Both support incognito contexts
Multiple Tabs Yes Yes Both support multiple pages
Network Interception Yes Yes Similar APIs
Geolocation Yes Yes Similar APIs
Emulation Yes Yes Device emulation, viewport
Browser Support Chromium only Chromium only Firefox/Safari not yet supported
TypeScript Types Yes Yes Both fully typed

Session Management

Puppeteer (Advanced)

// List active sessions
const sessions = await puppeteer.sessions(env.MYBROWSER);

// Find free session
const freeSession = sessions.find(s => !s.connectionId);

// Connect to existing session
if (freeSession) {
  const browser = await puppeteer.connect(env.MYBROWSER, freeSession.sessionId);
}

// Check limits
const limits = await puppeteer.limits(env.MYBROWSER);
console.log("Can launch:", limits.allowedBrowserAcquisitions > 0);

// View history
const history = await puppeteer.history(env.MYBROWSER);

Puppeteer APIs:

  • puppeteer.sessions() - List active sessions
  • puppeteer.connect() - Connect to session by ID
  • puppeteer.history() - View recent sessions
  • puppeteer.limits() - Check account limits
  • browser.sessionId() - Get current session ID
  • browser.disconnect() - Disconnect without closing

Playwright (Basic)

// Launch browser
const browser = await chromium.launch(env.BROWSER);

// Get session info (basic)
// Note: No .sessions(), .history(), or .limits() APIs

Playwright APIs:

  • No chromium.sessions() equivalent
  • No session reuse APIs
  • No limits checking
  • No session history

Workaround: Use Puppeteer-style session management via REST API (more complex).


Auto-Waiting Behavior

Puppeteer (Manual)

// Must explicitly wait for elements
await page.goto("https://example.com");
await page.waitForSelector("button#submit");
await page.click("button#submit");

Pros: Fine-grained control

Cons: More verbose, easy to forget waits


Playwright (Auto-waiting)

// Automatically waits for elements
await page.goto("https://example.com");
await page.click("button#submit");  // Waits automatically!

Pros: Less boilerplate, fewer timing issues

Cons: Less control over wait behavior


Selector Support

Puppeteer

Supported:

  • CSS selectors: "button#submit", "div > p"
  • :visible, :hidden pseudo-classes
  • page.$(), page.$$() for querying

Not Supported:

  • XPath selectors (use page.evaluate() workaround)
  • Text selectors
  • Layout selectors

Example:

// CSS selector
const button = await page.$("button#submit");

// XPath workaround
const heading = await page.evaluate(() => {
  return new XPathEvaluator()
    .createExpression("//h1[@class='title']")
    .evaluate(document, XPathResult.FIRST_ORDERED_NODE_TYPE)
    .singleNodeValue.textContent;
});

Playwright

Supported:

  • CSS selectors: "button#submit"
  • Text selectors: "text=Submit"
  • XPath selectors: "xpath=//button"
  • Layout selectors: "button :right-of(:text('Cancel'))"

Example:

// CSS selector
await page.click("button#submit");

// Text selector
await page.click("text=Submit");

// Combined selector
await page.click("button >> text=Submit");

Advantage: More flexible selector options


Performance & Cost

Puppeteer (Optimized)

Session Reuse:

// Reuse sessions to reduce costs
const sessions = await puppeteer.sessions(env.MYBROWSER);
const browser = await puppeteer.connect(env.MYBROWSER, sessionId);
await browser.disconnect(); // Keep alive

Cost Impact:

  • Reduce cold starts by 50-70%
  • Lower concurrency charges
  • Better throughput

Playwright (Limited Optimization)

No Session Reuse:

// Must launch new browser each time
const browser = await chromium.launch(env.BROWSER);
await browser.close(); // Cannot keep alive for reuse

Cost Impact:

  • Higher browser hours (cold starts every request)
  • Higher concurrency usage
  • Lower throughput

Difference: ~30-50% higher costs with Playwright vs optimized Puppeteer.


API Differences

Operation Puppeteer Playwright
Import import puppeteer from "@cloudflare/puppeteer" import { chromium } from "@cloudflare/playwright"
Launch puppeteer.launch(env.MYBROWSER) chromium.launch(env.BROWSER)
Connect puppeteer.connect(env.MYBROWSER, sessionId) Not available
Sessions puppeteer.sessions(env.MYBROWSER) Not available
Limits puppeteer.limits(env.MYBROWSER) Not available
Goto page.goto(url, { waitUntil: "networkidle0" }) page.goto(url, { waitUntil: "networkidle" })
Screenshot page.screenshot({ fullPage: true }) page.screenshot({ fullPage: true })
PDF page.pdf({ format: "A4" }) page.pdf({ format: "A4" })
Wait page.waitForSelector("button") page.locator("button").waitFor()
Click page.click("button") page.click("button") (auto-waits)

Migration Guide

Puppeteer → Playwright

// Before (Puppeteer)
import puppeteer from "@cloudflare/puppeteer";

const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
await page.goto(url, { waitUntil: "networkidle0" });
await page.waitForSelector("button#submit");
await page.click("button#submit");
const screenshot = await page.screenshot();
await browser.close();
// After (Playwright)
import { chromium } from "@cloudflare/playwright";

const browser = await chromium.launch(env.BROWSER);
const page = await browser.newPage();
await page.goto(url, { waitUntil: "networkidle" });
// No waitForSelector needed - auto-waits
await page.click("button#submit");
const screenshot = await page.screenshot();
await browser.close();

Changes:

  1. Import: puppeteer{ chromium }
  2. Launch: puppeteer.launch()chromium.launch()
  3. Wait: networkidle0networkidle
  4. Remove explicit waitForSelector() (auto-waits)

Playwright → Puppeteer

// Before (Playwright)
import { chromium } from "@cloudflare/playwright";

const browser = await chromium.launch(env.BROWSER);
const page = await browser.newPage();
await page.goto(url);
await page.click("button#submit"); // Auto-waits
// After (Puppeteer)
import puppeteer from "@cloudflare/puppeteer";

const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
await page.goto(url, { waitUntil: "networkidle0" });
await page.waitForSelector("button#submit"); // Explicit wait
await page.click("button#submit");

Changes:

  1. Import: { chromium }puppeteer
  2. Launch: chromium.launch()puppeteer.launch()
  3. Add explicit waits: page.waitForSelector()
  4. Specify wait conditions: waitUntil: "networkidle0"

Use Case Recommendations

Screenshot Service

Winner: Puppeteer

Reason: Session reuse reduces costs by 30-50%

// Puppeteer: Reuse sessions
const sessions = await puppeteer.sessions(env.MYBROWSER);
const browser = await puppeteer.connect(env.MYBROWSER, sessionId);
await browser.disconnect(); // Keep alive

PDF Generation

Winner: Tie

Reason: Identical API, no session reuse benefit

// Both have same API
const pdf = await page.pdf({ format: "A4" });

Web Scraping

Winner: Puppeteer

Reason: Session management + limit checking

// Puppeteer: Check limits before scraping
const limits = await puppeteer.limits(env.MYBROWSER);
if (limits.allowedBrowserAcquisitions === 0) {
  await delay(limits.timeUntilNextAllowedBrowserAcquisition);
}

Test Migration

Winner: Playwright

Reason: Easier to migrate existing Playwright tests

// Minimal changes needed
// Just update imports and launch

Interactive Automation

Winner: Tie

Reason: Both support form filling, clicking, etc.


Configuration

wrangler.jsonc (Puppeteer)

{
  "browser": {
    "binding": "MYBROWSER"
  },
  "compatibility_flags": ["nodejs_compat"]
}
interface Env {
  MYBROWSER: Fetcher;
}

wrangler.jsonc (Playwright)

{
  "browser": {
    "binding": "BROWSER"
  },
  "compatibility_flags": ["nodejs_compat"]
}
interface Env {
  BROWSER: Fetcher;
}

Note: Binding name is arbitrary, but convention is MYBROWSER for Puppeteer and BROWSER for Playwright.


Production Considerations

Puppeteer Advantages

  • Session reuse (30-50% cost savings)
  • Limit checking (puppeteer.limits())
  • Session monitoring (puppeteer.sessions(), .history())
  • Better performance optimization options
  • More mature Cloudflare fork

Playwright Advantages

  • Auto-waiting (less code)
  • More selector types
  • Better cross-browser APIs (future-proof)
  • Easier migration from existing tests

Recommendation Summary

Scenario Recommended Reason
New project Puppeteer Session management + cost optimization
Screenshot service Puppeteer Session reuse saves 30-50%
PDF generation Tie Identical API
Web scraping Puppeteer Limit checking + session management
Migrating Playwright tests Playwright Minimal changes needed
High traffic production Puppeteer Better performance optimization
Quick prototype Tie Both easy to start with

Code Examples

Puppeteer (Production-Optimized)

import puppeteer, { Browser } from "@cloudflare/puppeteer";

async function getBrowser(env: Env): Promise<Browser> {
  // Check limits
  const limits = await puppeteer.limits(env.MYBROWSER);
  if (limits.allowedBrowserAcquisitions === 0) {
    throw new Error("Rate limit reached");
  }

  // Try to reuse session
  const sessions = await puppeteer.sessions(env.MYBROWSER);
  const freeSession = sessions.find(s => !s.connectionId);

  if (freeSession) {
    try {
      return await puppeteer.connect(env.MYBROWSER, freeSession.sessionId);
    } catch {
      // Session closed, launch new
    }
  }

  return await puppeteer.launch(env.MYBROWSER);
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const browser = await getBrowser(env);

    try {
      const page = await browser.newPage();
      await page.goto("https://example.com", {
        waitUntil: "networkidle0",
        timeout: 30000
      });
      const screenshot = await page.screenshot();

      // Disconnect (keep alive)
      await browser.disconnect();

      return new Response(screenshot, {
        headers: { "content-type": "image/png" }
      });
    } catch (error) {
      await browser.close();
      throw error;
    }
  }
};

Playwright (Simple)

import { chromium } from "@cloudflare/playwright";

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const browser = await chromium.launch(env.BROWSER);

    try {
      const page = await browser.newPage();
      await page.goto("https://example.com", {
        waitUntil: "networkidle",
        timeout: 30000
      });
      const screenshot = await page.screenshot();

      await browser.close();

      return new Response(screenshot, {
        headers: { "content-type": "image/png" }
      });
    } catch (error) {
      await browser.close();
      throw error;
    }
  }
};

References


Last Updated: 2025-10-22