[{"data":1,"prerenderedAt":16},["ShallowReactive",2],{"$fgJYYR-lBKS2mRZKp9YYIFtiWkD7WE6gosgpc-Ex5eTI":3},{"slug":4,"title":5,"description":6,"date":7,"tags":8,"draft":13,"readingMinutes":14,"html":15},"puppeteer-vs-playwright","Puppeteer vs Playwright: Which One I'd Pick in 2026","Puppeteer vs Playwright, checked October 2026: browsers, waiting, the test runner, PDFs and installs, and which one fits testing, automation or a PDF service.","2026-10-18",[9,10,11,12],"puppeteer","playwright","node","testing",false,4,"\u003Cp>If you searched &quot;puppeteer vs playwright&quot; because a test just failed with \u003Ccode>TimeoutError: Waiting for selector .welcome failed: Waiting failed: 30000ms exceeded\u003C/code>, I can save you some reading: that error isn&#39;t really Puppeteer&#39;s fault, and switching to Playwright will mostly make it go away anyway. The rest of this post is why.\u003C/p>\n\u003Cp>As of October 18, 2026, \u003Ccode>puppeteer\u003C/code> is at 25.14.0 and \u003Ccode>playwright\u003C/code> at 1.64.1 on npm. Both shipped a release on October 14, so neither is dying. npm counted about 6.7 million downloads of \u003Ccode>puppeteer\u003C/code> last week and about 69 million of \u003Ccode>playwright\u003C/code>, 46 million of those for \u003Ccode>@playwright/test\u003C/code>. Playwright won testing. Puppeteer is still what a lot of Node code uses to drive Chrome for everything else, PDFs especially.\u003C/p>\n\u003Cimg src=\"/blog/puppeteer-vs-playwright/puppeteer-vs-playwright-same-test.webp\" alt=\"The same login test in Puppeteer and Playwright, side by side, with the lines where each one waits for the page marked\" width=\"1600\" height=\"900\" loading=\"lazy\">\u003Ch2>That timeout, and why Playwright mostly doesn&#39;t have it\u003C/h2>\n\u003Cp>Look at the left side of the picture. Puppeteer code written the common way has explicit waits: \u003Ccode>waitForSelector\u003C/code> before you type, \u003Ccode>waitForNavigation\u003C/code> after you click. Each one is a guess about when the page is ready. The classic bug is the \u003Ccode>click\u003C/code> followed by \u003Ccode>waitForNavigation\u003C/code>: if the navigation finishes before the wait starts, the wait sits there for 30 seconds and fails. The fix people find on Stack Overflow is to wrap both in \u003Ccode>Promise.all\u003C/code>. The fix people actually commit is \u003Ccode>await new Promise(r =&gt; setTimeout(r, 2000))\u003C/code>, and now the test is slow and still flaky on a busy CI box.\u003C/p>\n\u003Cp>Playwright&#39;s locators wait for you. \u003Ccode>page.getByRole(&#39;button&#39;, { name: &#39;Sign in&#39; }).click()\u003C/code> waits until the button exists, is visible, is enabled and has stopped animating. Its \u003Ccode>expect\u003C/code> keeps retrying until the timeout instead of checking once. You still get timeouts, they just read differently: \u003Ccode>locator.click: Timeout 30000ms exceeded\u003C/code>. And you get one error Puppeteer never gives you, \u003Ccode>strict mode violation: getByRole(&#39;button&#39;, { name: &#39;Save&#39; }) resolved to 2 elements\u003C/code>, which is Playwright refusing to guess which button you meant. Annoying the first week. Then you notice it caught a real bug.\u003C/p>\n\u003Cp>To be fair to Puppeteer: it has had its own locator API since mid-2023, with the same kind of waiting. Almost nobody uses it, because every tutorial and every inherited codebase predates it.\u003C/p>\n\u003Ch2>The runner matters more than the library\u003C/h2>\n\u003Cp>\u003Ccode>@playwright/test\u003C/code> is the part people leave out of the comparison. Parallel workers, retries, fixtures, a project per browser and phone size, an HTML report. And the trace viewer: turn on \u003Ccode>trace: &#39;on-first-retry&#39;\u003C/code> and a failed CI run leaves a zip with every action, a DOM snapshot before and after it, network calls and console output. You open it with \u003Ccode>npx playwright show-trace\u003C/code> and scrub through the failure like a video. For anyone who has debugged a test that only fails in CI, that one feature is the argument.\u003C/p>\n\u003Cp>Puppeteer has no runner. You put it under Jest or Vitest, write your own setup and teardown, and decide yourself what a retry means.\u003C/p>\n\u003Ch2>Browsers\u003C/h2>\n\u003Cp>Puppeteer is the Chrome team&#39;s. Since v20 it downloads Chrome for Testing (25.14.0 pairs with 155.0.8059.79), since v23 it drives stable Firefox through WebDriver BiDi, and it&#39;s JavaScript only. Playwright ships its own Chromium, Firefox and WebKit builds, can use branded Chrome or Edge through \u003Ccode>channel\u003C/code>, and has official Python, Java and .NET versions. Its docs are honest that their WebKit isn&#39;t Safari, and say to run it on a Mac when you need the closest thing. On Linux CI it still catches plenty of Safari layout bugs that a Chromium-only suite never sees.\u003C/p>\n\u003Ch2>PDFs, which is where Puppeteer earns its keep\u003C/h2>\n\u003Cp>Both render PDFs through Chromium with \u003Ccode>page.pdf()\u003C/code> (in Playwright it only works in Chromium). For an HTML-to-PDF service I&#39;m happy with Puppeteer. It&#39;s one browser and one protocol client, and if you&#39;re on Laravel you&#39;re probably on it already, because Spatie&#39;s Browsershot runs Puppeteer underneath.\u003C/p>\n\u003Cp>The problems with a PDF service are rarely the library. They&#39;re the server. On a slim Linux image the first invoice comes out with empty squares where the text should be, because there are no fonts installed. A service that launches a browser per request falls over at a few dozen concurrent PDFs, and one that forgets to close pages in a \u003Ccode>finally\u003C/code> collects zombie Chrome processes until the box runs out of memory. Launch one browser, reuse it, open and close a page per job, and put a queue in front of it.\u003C/p>\n\u003Cp>Installs are similar on both sides now. \u003Ccode>npx playwright install\u003C/code> pulls all three browsers unless you name one, so in CI use \u003Ccode>npx playwright install --with-deps --only-shell chromium\u003C/code>. Puppeteer split the old headless mode out into \u003Ccode>chrome-headless-shell\u003C/code>, which you get with \u003Ccode>headless: &#39;shell&#39;\u003C/code>.\u003C/p>\n\u003Ch2>So\u003C/h2>\n\u003Cp>For end-to-end tests of a web app, Playwright and its runner, and I wouldn&#39;t spend long deciding. For a Node service that makes PDFs or screenshots, keep Puppeteer if you have it, start with either if you don&#39;t. For a quick script that fills in the same form every morning, use whichever you know, though \u003Ccode>npx playwright codegen\u003C/code> recording your clicks into code is hard to beat. If half your team writes Python, that decides it for you: Playwright. (I do scraping too, with various methods and techniques to avoid blockers, but that&#39;s a different post.)\u003C/p>\n\u003Cp>Stuck with a flaky suite, or a PDF service that falls over under load? \u003Ca href=\"https://calendly.com/director-norsky/meeting\">Book a free call\u003C/a>. All consultation is free.\u003C/p>\n",1792284822091]