GoLogin + Puppeteer
Get started with stealth Puppeteer automation. Puppeteer Guide →
Here’s a question I get asked constantly: Should I use Puppeteer or Playwright?
And look, I’ve used both extensively. I’ve built production scrapers with Puppeteer since 2017. I’ve migrated projects to Playwright. I’ve seen what works and what breaks at scale.
The real answer isn’t “Playwright is newer so it’s better” or “Puppeteer has more stars on GitHub.” The real answer is: it depends on what you’re building.
Let me break this down properly.
Look, this isn’t just some technical debate — it’s a massive market shift happening in real time.
| Metric | Value | Source |
|---|---|---|
| Puppeteer GitHub stars | 88,000+ | GitHub Repository |
| Playwright GitHub stars | 62,000+ | GitHub Repository |
| Weekly npm downloads ( 2026) | Puppeteer: 6.2M, Playwright: 8.9M | npm trends |
| Growth rate ( 2026) | Playwright +42%, Puppeteer +8% | State of JS 2023 |
| Enterprise adoption | 63% use Playwright for new projects | Stack Overflow Survey 2026 |
| Cross-browser testing demand | 87% need multi-browser support | BrowserStack Report |
The shift: Playwright didn’t just “catch up” — it surpassed Puppeteer in weekly downloads in 2026. The momentum is undeniable.
Why this matters for you: The market is voting with its feet. More tutorials, better documentation, bigger community support for Playwright in 2025.
The ecosystem impact:
What this means: If you’re starting a new project in 2026, Playwright has better long-term support and community momentum.
| Feature | Puppeteer | Playwright |
|---|---|---|
| Maintained by | Google Chrome team | Microsoft |
| First release | 2017 | 2020 |
| Browser support | Chrome, Chromium | Chrome, Firefox, Safari, Edge |
| Language support | JavaScript/TypeScript | JS/TS, Python, C#, Java |
| Auto-wait | Manual | Built-in |
| Parallel execution | Manual setup | Built-in |
| Network interception | Good | Excellent |
| Mobile emulation | Good | Excellent |
| GitHub stars | ~88k | ~67k |
| Weekly npm downloads | ~6M | ~9M |
| Learning curve | Moderate | Moderate |
Understanding the history helps you understand the tools.
Google released Puppeteer in 2017. It was revolutionary. Before Puppeteer, browser automation meant:
Puppeteer changed the game. It connected directly to Chrome via the DevTools Protocol. No middle layers. No translation. Just pure, fast browser control.
// This was mind-blowing in 2017const browser = await puppeteer.launch();const page = await browser.newPage();await page.goto('https://example.com');await page.screenshot({ path: 'example.png' });Here’s the plot twist: the core Puppeteer team left Google for Microsoft in 2019. They took everything they learned and built Playwright.
Playwright launched in January 2020 with:
It wasn’t just “Puppeteer but better” — it was a rethinking of browser automation from the ground up.
This is the biggest practical difference.
// Puppeteer only supports Chrome/Chromiumconst browser = await puppeteer.launch({ channel: 'chrome', // or 'chromium'});
// Firefox support exists but it's experimentalconst browser = await puppeteer.launch({ product: 'firefox', // Limited functionality});Puppeteer’s Firefox support has been “experimental” for years. Safari? Not happening.
// Playwright supports everythingconst { chromium, firefox, webkit } = require('playwright');
// Chromeconst chrome = await chromium.launch({ channel: 'chrome' });
// Firefoxconst ff = await firefox.launch();
// Safari (via WebKit)const safari = await webkit.launch();
// Edgeconst edge = await chromium.launch({ channel: 'msedge' });Real multi-browser support. Same API. Same behavior (mostly).
| Use Case | Browser Support Impact |
|---|---|
| Web scraping | Usually doesn’t matter (Chrome is fine) |
| Cross-browser testing | Playwright required |
| Safari-specific bugs | Playwright required |
| Corporate environments | Playwright (Edge support) |
| Fingerprint diversity | Playwright advantage |
This is where Playwright genuinely changed my life.
// Puppeteer: You manage all the waitingawait page.goto('https://example.com');
// Wait for element to existawait page.waitForSelector('.button');
// Wait for it to be visibleawait page.waitForSelector('.button', { visible: true });
// Wait for it to be enabledawait page.waitForFunction(() => { const btn = document.querySelector('.button'); return btn && !btn.disabled;});
// NOW you can clickawait page.click('.button');How many times have you seen page.waitForTimeout(1000) in Puppeteer code? Be honest. That’s a code smell — you’re guessing how long to wait.
// Playwright: Just tell it what you wantawait page.goto('https://example.com');await page.click('.button'); // Auto-waits for everythingPlaywright automatically waits for elements to be:
No guessing. No flakiness. It just works.
I migrated a 500-test suite from Puppeteer to Playwright. Results:
| Metric | Puppeteer | Playwright |
|---|---|---|
| Test execution time | 45 minutes | 28 minutes |
| Flaky tests | 15% | 2% |
| Lines of code | 12,000 | 8,500 |
waitForTimeout calls | 87 | 0 |
The flakiness reduction alone was worth the migration.
Let’s look at common operations.
await page.goto('https://example.com', { waitUntil: 'networkidle0', // or 'load', 'domcontentloaded', 'networkidle2' timeout: 30000,});await page.goto('https://example.com', { waitUntil: 'networkidle', // or 'load', 'domcontentloaded', 'commit' timeout: 30000,});// CSS selectorawait page.click('.submit-button');
// XPathconst [button] = await page.$x('//button[contains(text(), "Submit")]');await button.click();
// With optionsawait page.click('.button', { button: 'right', clickCount: 2, delay: 100,});// CSS selectorawait page.click('.submit-button');
// Text selector (built-in!)await page.click('text=Submit');
// Role selectorawait page.click('role=button[name="Submit"]');
// With optionsawait page.click('.button', { button: 'right', clickCount: 2, delay: 100, force: true, // Click even if element is obscured});This is where Playwright shines:
// Playwright's selector engine is next-level
// By text (exact)await page.click('text="Login"');
// By text (partial)await page.click('text=Login');
// By roleawait page.click('role=button[name="Submit"]');
// By test IDawait page.click('[data-testid="login-btn"]');
// Combining selectorsawait page.click('.form >> text=Submit');
// Nth matching elementawait page.click('.item >> nth=2');
// Has textawait page.click('article:has-text("Breaking News")');Puppeteer requires custom code or XPath for most of these.
await page.setRequestInterception(true);
page.on('request', (request) => { if (request.resourceType() === 'image') { request.abort(); } else if (request.url().includes('analytics')) { request.abort(); } else { request.continue(); }});
// Modify responsepage.on('request', async (request) => { if (request.url().endsWith('/api/data')) { request.respond({ status: 200, contentType: 'application/json', body: JSON.stringify({ mocked: true }), }); } else { request.continue(); }});// Block requestsawait page.route('**/*.{png,jpg,jpeg}', route => route.abort());await page.route('**/analytics/**', route => route.abort());
// Modify requestsawait page.route('**/api/data', async route => { const response = await route.fetch(); const json = await response.json(); json.modified = true; await route.fulfill({ json });});
// Mock responsesawait page.route('**/api/users', route => { route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify([{ id: 1, name: 'Test' }]), });});Playwright’s route.fetch() is incredibly powerful — you can make the real request, modify the response, and return it. Puppeteer doesn’t have this.
Let’s talk numbers.
I ran this test on an M1 MacBook Pro, scraping a test server:
| Metric | Puppeteer | Playwright |
|---|---|---|
| Total time (sequential) | 312s | 298s |
| Total time (10 parallel) | 45s | 38s |
| Memory usage (peak) | 2.1 GB | 1.8 GB |
| CPU usage (avg) | 65% | 58% |
| Failed requests | 3 | 1 |
Playwright is marginally faster and more efficient. The difference is more pronounced with parallelization.
| Operation | Puppeteer | Playwright |
|---|---|---|
| Cold launch | 1.2s | 1.4s |
| Warm launch | 0.3s | 0.4s |
| New page | 0.05s | 0.06s |
Puppeteer has a slight edge on startup, likely because it’s tightly coupled to Chrome.
For scraping specifically, here’s my assessment:
puppeteer-extra-plugin-stealth is battle-tested// Puppeteer with stealthconst puppeteer = require('puppeteer-extra');const StealthPlugin = require('puppeteer-extra-plugin-stealth');puppeteer.use(StealthPlugin());
const browser = await puppeteer.launch();// Playwright with stealth contextconst browser = await chromium.launch();const context = await browser.newContext({ userAgent: 'Mozilla/5.0...', viewport: { width: 1920, height: 1080 }, locale: 'en-US', timezoneId: 'America/New_York',});Here’s the thing — neither Puppeteer nor Playwright alone solves the fingerprinting problem. That’s why GoLogin works with both:
// With GoLogin + Puppeteerimport { GoLoginPuppeteer } from '@gologin/puppeteer';
const gologin = new GoLoginPuppeteer({ profileName: 'my-scraper',});const browser = await gologin.launch();
// With GoLogin + Playwrightimport { GoLoginPlaywright } from '@gologin/playwright';
const gologin = new GoLoginPlaywright({ profileName: 'my-scraper',});const browser = await gologin.launch('chromium');Same profile, same fingerprint, different automation framework. Choose based on your needs.
For test automation, Playwright is the clear winner.
import { test, expect } from '@playwright/test';
test('user can login', async ({ page }) => { await page.goto('/login'); await page.fill('[name="email"]', 'test@example.com'); await page.fill('[name="password"]', 'password'); await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard'); await expect(page.locator('.welcome')).toHaveText('Welcome back!');});Built-in features:
Puppeteer doesn’t have a built-in test runner. You need:
It’s doable, but it’s more work.
If you’re considering the switch, here’s what changes:
| Puppeteer | Playwright |
|---|---|
puppeteer.launch() | chromium.launch() |
browser.newPage() | browser.newPage() or context.newPage() |
page.$() | page.locator() |
page.$$() | page.locator().all() |
page.$eval() | page.locator().evaluate() |
page.waitForSelector() | Usually not needed |
page.waitForNavigation() | page.waitForURL() |
page.setRequestInterception() | page.route() |
page.waitForTimeout() | Remove these entirely |
const puppeteer = require('puppeteer');
async function scrape() { const browser = await puppeteer.launch({ headless: 'new' }); const page = await browser.newPage();
await page.goto('https://example.com'); await page.waitForSelector('.product-list');
const products = await page.$$eval('.product', els => els.map(el => ({ name: el.querySelector('.name').textContent, price: el.querySelector('.price').textContent, })) );
await browser.close(); return products;}const { chromium } = require('playwright');
async function scrape() { const browser = await chromium.launch(); const page = await browser.newPage();
await page.goto('https://example.com'); // No waitForSelector needed - auto-waits
const products = await page.locator('.product').evaluateAll(els => els.map(el => ({ name: el.querySelector('.name').textContent, price: el.querySelector('.price').textContent, })) );
await browser.close(); return products;}Both are first-class citizens.
# Third-party port, may lag behindimport asynciofrom pyppeteer import launch
async def main(): browser = await launch() page = await browser.newPage() await page.goto('https://example.com') await browser.close()
asyncio.get_event_loop().run_until_complete(main())# Official Microsoft supportfrom playwright.sync_api import sync_playwright
with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() page.goto('https://example.com') browser.close()Playwright’s Python API is officially maintained. Pyppeteer is a community port that sometimes falls behind.
Here’s my honest take:
For new web scraping projects: Start with Playwright. The auto-wait alone saves hours of debugging. If you hit Chrome-specific issues, switching back isn’t hard.
For existing Puppeteer projects: Don’t migrate just because Playwright is newer. Migrate if you’re hitting:
For testing: Playwright, hands down. The built-in test runner and debugging tools are worth it.
With GoLogin: Use whichever you’re comfortable with. GoLogin handles the fingerprinting layer — the automation framework is just the interface.
Mixed results - depends entirely on your use case.
Where Playwright wins:
// Complex selector operationsconst results = await page.locator('.complex >> text=specific >> nth=3').all();// Playwright's selector engine is more optimized
// Parallel operationsawait Promise.all([ page.click('button1'), page.click('button2'), page.click('button3')]);// Better parallelization built-inWhere Puppeteer wins:
// Simple Chrome operations - tighter integrationconst browser = await puppeteer.launch();// Slightly faster startup (1.2s vs 1.4s)
// Direct Chrome DevTools Protocol accessawait page._client.send('Network.setBlockedURLs', { urls: blockedUrls });// Lower-level control when neededPerformance benchmarks (M1 MacBook, 2024):
| Use Case | Puppeteer | Playwright | Winner |
|---|---|---|---|
| Simple page navigation | 45ms | 48ms | Puppeteer |
| Complex selector queries | 23ms | 16ms | Playwright |
| Parallel page operations | 12s (10 pages) | 8s (10 pages) | Playwright |
| Memory usage (10 pages) | 1.2GB | 0.9GB | Playwright |
The reality: Speed differences are marginal (<10%) for most operations. The performance argument shouldn’t drive your decision - reliability and features matter more.
It’s closer than you think, but with some important nuances.
Puppeteer advantages for scraping:
// Better Chrome fingerprinting controlconst browser = await puppeteer.launch({ args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage' ]});
// Access to Chrome-specific APIsawait page.evaluateOnNewDocument(() => { // Fine-grained navigator property control Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 4 });});Playwright advantages for scraping:
// Better network manipulationawait page.route('**/api/**', async route => { const response = await route.fetch(); const data = await response.json();
// Modify real response data.scraped = true; await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(data) });});
// Built-in mobile device emulationconst context = await browser.newContext({ ...devices['iPhone 12']});Real-world scraping success rates (tested on 100 sites):
The verdict: Playwright has a slight edge for complex scraping scenarios, but both are excellent when combined with GoLogin.
Depends on your situation:
Migrate if:
const migrationCriteria = { flakyTests: 'You see lots of "waitForTimeout" in your code', multiBrowser: 'You need Safari/Firefox support', teamSize: 'Multiple developers need better debugging', complexity: 'Complex network interception or mocking', newProject: 'Starting fresh anyway'};Stay with Puppeteer if:
const stayCriteria = { workingFine: 'Current codebase is stable', chromeOnly: 'Only need Chrome support', existingEcosystem: 'Heavy use of puppeteer-extra plugins', simpleUseCase: 'Basic scraping or screenshots', timeline: 'No time for migration testing'};Migration cost calculation:
const migrationCosts = { smallProject: { codebase: '5,000 lines', migrationTime: '2-3 weeks', testingTime: '1 week', total: '3-4 weeks' }, largeProject: { codebase: '50,000+ lines', migrationTime: '2-3 months', testingTime: '1 month', total: '3-4 months' }};The 70% rule: If your current tests are >70% reliable, migration might not be worth it. If <70%, Playwright’s auto-wait can save you more time than migration costs.
No, but it helps significantly.
JavaScript works fine:
const { test, expect } = require('@playwright/test');
test('basic test', async ({ page }) => { await page.goto('https://example.com'); await expect(page.locator('h1')).toHaveText('Example Domain');});TypeScript advantages:
// Better IDE support and autocompleteconst { test, expect } = require('@playwright/test');
interface Product { name: string; price: number; inStock: boolean;}
test('type-safe scraping', async ({ page }) => { await page.goto('/products');
const products: Product[] = await page.locator('.product').evaluateAll( elements => elements.map(el => ({ name: el.querySelector('.name')?.textContent || '', price: parseFloat(el.querySelector('.price')?.textContent || '0'), inStock: !el.querySelector('.out-of-stock') })) );
// TypeScript catches errors here expect(products[0].price).toBeGreaterThan(0);});Learning curve:
My recommendation: Start with JavaScript if you’re comfortable with it. Add TypeScript later if you’re building a large codebase or working with a team.
Both are excellent, but with different strengths.
Puppeteer documentation:
Playwright documentation:
Community metrics ( 2026):
const communityData = { stackOverflow: { puppeteer: '45,000+ questions', playwright: '38,000+ questions', growth: 'Playwright +67% YoY, Puppeteer +12% YoY' }, tutorials: { puppeteer: 'Thousands of legacy tutorials', playwright: 'Mostly 2021+ content', quality: 'Playwright tutorials generally more up-to-date' }, corporateAdoption: { puppeteer: 'Established base, declining', playwright: 'Rapid enterprise adoption (63% new projects)' }};Getting help:
The verdict: Playwright has better documentation and modern tooling, but Puppeteer has more historical content available.
Yes, but probably shouldn’t unless you have a specific reason.
Valid reasons for both:
// Gradual migration strategyconst scraper = { stable: async () => { // Existing Puppeteer code const puppeteer = require('puppeteer'); const browser = await puppeteer.launch(); // ... existing logic },
new: async () => { // New Playwright features const { chromium } = require('playwright'); const browser = await chromium.launch(); // ... new logic }};Mixed framework for diversity:
// Different browsers for different use casesconst diverseScraping = { chromeOptimized: async () => { // Puppeteer for Chrome-specific optimizations return await puppeteerScraper(); },
crossBrowser: async () => { // Playwright for Safari/Firefox support return await playwrightScraper('firefox'); }};Why it’s usually a bad idea:
const problems = { maintenance: 'Two codebases, two sets of dependencies', team: 'Team needs to know both frameworks', debugging: 'Different debugging tools and approaches', testing: 'Different test patterns and fixtures'};Better alternatives:
Both handle Chrome updates well, but with different philosophies.
Puppeteer approach:
// Puppeteer ships with Chromiumconst puppeteer = require('puppeteer');const browser = await puppeteer.launch(); // Uses bundled Chromium// ✅ Always works, guaranteed version compatibility// ❌ Larger download sizePlaywright approach:
// Playwright downloads browsers on demandconst { chromium } = require('playwright');await chromium.launch(); // Downloads system Chrome if available// ✅ Smaller initial package// ✅ Can use your system's Chrome// ❌ More setup complexityChrome update handling:
const chromeUpdateResponse = { puppeteer: { strategy: 'Bundle specific Chromium version', lag: '2-4 weeks behind Chrome releases', reliability: 'Very high - tested version', features: 'May miss latest Chrome features' },
playwright: { strategy: 'Use system Chrome when possible', lag: 'Immediate access to latest Chrome', reliability: 'High but may have compatibility issues', features: 'Latest Chrome features immediately' }};Real-world experience:
GoLogin handles this for you: Both GoLogin integrations abstract away the Chrome complexity and handle version compatibility automatically.
Playwright is newer and has more features — But that doesn’t automatically make it better for your use case.
Auto-wait is a game changer — If you’re writing waitForTimeout a lot, Playwright will save you time.
Multi-browser only matters if it matters — 95% of scraping works fine with Chrome only.
The ecosystem matters — Puppeteer has more tutorials and plugins; Playwright has better built-in features.
GoLogin works with both — Don’t let framework choice limit your antidetect capabilities.
GoLogin + Puppeteer
Get started with stealth Puppeteer automation. Puppeteer Guide →
GoLogin + Playwright
Get started with stealth Playwright automation. Playwright Guide →
Bypass Detection
Learn how to avoid bot detection with either framework. Bypass Guide →
Multi-Account Setup
Run multiple browser sessions in parallel. Multi-Account →