Playwright vs Puppeteer for Screenshots of Authenticated Web Pages
Compare Playwright and Puppeteer for authenticated screenshots, with reusable login state, runnable capture examples, troubleshooting, and a hosted API option.
Short answer: Playwright is usually the more direct choice when you need to sign in once, save authenticated browser state, and reuse it for screenshot runs. Puppeteer can capture authenticated pages too, but you establish the site’s session through its actual login flow or browser storage yourself. Both support page and element screenshots. If visual regression baselines are part of the job, Playwright Test also provides a screenshot assertion that waits for consecutive captures to stabilize.
Choose based on the authentication mechanism and how you need to manage session state. Neither library is a universal speed or reliability winner: the official documentation reviewed does not provide same-workload comparative measurements.
What “authenticated screenshot” means
A browser screenshot is authenticated only if the browser context has the state the site expects when it loads the page. That state may come from cookies, local storage, IndexedDB, an HTTP authentication challenge, or a form-based login. A screenshot API call by itself does not log into an arbitrary web application.
Keep these cases distinct:
- Application login: A page or identity provider accepts credentials, then the site establishes a browser session. Use the real login flow or restore the state it creates.
- HTTP authentication: The server asks the browser for credentials at the HTTP layer. Puppeteer’s
Page.authenticate()is specifically for this case. - Reusable browser state: Cookies and browser storage established in one run are saved and loaded into a later context.
For either tool, wait for a meaningful signed-in condition before capture. A successful navigation alone does not prove the login completed or that the page’s data has loaded.
Playwright vs Puppeteer at a glance
| Need | Playwright | Puppeteer |
|---|---|---|
| Save and reuse login state | Documented storageState workflow. The guide covers cookies, local storage, IndexedDB, and passkey/WebAuthn-based authentication. |
Browser contexts isolate state, and the API documents cookie controls. Build the persistence workflow around the site’s authentication and storage needs. |
| Capture a page | page.screenshot() supports output and rendering options. |
Page.screenshot() captures a page. |
| Capture one element | Use a locator or element screenshot workflow. | ElementHandle.screenshot() captures an element and scrolls it into view when needed. |
| Visual regression assertion | Playwright Test’s toHaveScreenshot() waits for two consecutive screenshots to match before comparing. It requires the Playwright test runner. |
The reviewed Puppeteer sources establish capture APIs, not an equivalent built-in baseline assertion. |
| HTTP authentication | Handle according to the browser context and the site’s auth setup. | Page.authenticate() supplies HTTP authentication credentials and enables request interception, which may affect performance. |
| Speed comparison | No general winner established by the sources reviewed. | No general winner established by the sources reviewed. |
Sources: Playwright authentication, Playwright Page API, Playwright PageAssertions API, Puppeteer screenshots, Puppeteer BrowserContext API, and Puppeteer Page API.
Playwright: authenticate once and reuse storage state
This example uses Playwright Test. The setup project signs in, verifies an authenticated page condition, then saves storage state. The screenshot test loads that state into its context.
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
projects: [
{
name: 'setup',
testMatch: /auth\.setup\.ts/,
},
{
name: 'chromium',
use: {
browserName: 'chromium',
storageState: 'playwright/.auth/user.json',
viewport: { width: 1440, height: 1000 },
},
dependencies: ['setup'],
},
],
});
// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import { mkdir } from 'node:fs/promises';
const authFile = 'playwright/.auth/user.json';
setup('sign in and save browser state', async ({ page, context }) => {
await mkdir('playwright/.auth', { recursive: true });
await page.goto('https://example.com/login');
await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
// Replace this with a stable signal unique to your signed-in page.
await expect(page.getByRole('navigation', { name: 'Account' })).toBeVisible();
await context.storageState({ path: authFile });
});
// tests/account.screenshot.spec.ts
import { test, expect } from '@playwright/test';
test('capture the authenticated account page', async ({ page }) => {
await page.goto('https://example.com/account');
await expect(page.getByRole('heading', { name: 'Account' })).toBeVisible();
await page.screenshot({ path: 'artifacts/account.png', fullPage: true });
});
Install the test runner with npm install --save-dev @playwright/test, then run npx playwright test. Set TEST_USER_EMAIL and TEST_USER_PASSWORD in the environment. Replace the example URL, labels, and assertions with the application’s real login flow and signed-in marker. Ensure the output directory exists if your workflow does not create it automatically.
The Playwright authentication guide warns that saved state can contain cookies and headers that allow account impersonation. Add playwright/.auth to .gitignore, use test-only accounts, and restrict access to the file. For ephemeral state, the test output directory can be used instead of a persistent auth directory.
storageState covers documented browser state including cookies, local storage, IndexedDB, and passkey/WebAuthn-based authentication. Session storage is a special case; it needs custom save and load handling. Confirm which storage mechanism your application uses before assuming the saved file is sufficient.
Puppeteer: establish the session, then capture
Puppeteer provides isolated browser contexts and cookie operations. This example performs a site’s form login in a fresh context, checks a signed-in marker, and captures the page. It deliberately performs the login per run; persistence and secure storage of a reusable session are application-specific.
// capture.mjs
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({ headless: true });
try {
const context = await browser.createBrowserContext();
const page = await context.newPage();
await page.setViewport({ width: 1440, height: 1000, deviceScaleFactor: 1 });
await page.goto('https://example.com/login', { waitUntil: 'domcontentloaded' });
await page.locator('input[name="email"]').fill(process.env.TEST_USER_EMAIL);
await page.locator('input[name="password"]').fill(process.env.TEST_USER_PASSWORD);
await page.locator('button[type="submit"]').click();
// Replace with a stable marker that appears only after successful login.
await page.waitForSelector('[data-testid="account-navigation"]', { visible: true });
await page.goto('https://example.com/account', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('h1[data-testid="account-title"]', { visible: true });
await page.screenshot({ path: 'account.png', fullPage: true });
await context.close();
} finally {
await browser.close();
}
Install Puppeteer with npm install puppeteer and run node capture.mjs. Provide credentials through environment variables, not source code. Adjust selectors and waits for the target site. If the session has already been established, create or select the context that owns it, then navigate and capture there. Context isolation matters: cookies in one context are not automatically available in another.
For HTTP authentication, use Puppeteer’s page method before navigation:
await page.authenticate({ username: process.env.HTTP_USER, password: process.env.HTTP_PASSWORD });
await page.goto('https://protected.example.com/report');
await page.screenshot({ path: 'report.png', fullPage: true });
This handles HTTP authentication challenges; it does not fill in a website login form. Puppeteer’s API notes that calling authenticate() enables request interception behind the scenes, which may affect performance.
Choose the screenshot scope and make it repeatable
Viewport, full page, or element
- Viewport: Captures what is currently visible. Fix the viewport size and device scale factor to make runs comparable.
- Full page: Captures the page beyond the initial viewport. Lazy-loaded content may require scrolling or other page-specific preparation before capture.
- Element: Useful for a chart, receipt, report, or component. Wait for the target to be visible and fully rendered first. Puppeteer’s element screenshot scrolls the element into view if needed.
Playwright screenshot options include output path and scale; Puppeteer offers page and element screenshot methods. See the respective Playwright Page API and Puppeteer screenshots guide for current option details.
Wait for the content you need
Use a condition tied to the page’s actual authenticated content, such as a heading, account navigation, or report row. If data loads after login, wait for that data too. A fixed delay can help with a known animation or delayed transition, but it is less reliable than waiting for a meaningful selector or state. Avoid treating network-idle as proof of authentication; pages with polling or persistent connections may never become idle.
Control sources of visual variation
- Use the same browser engine and version, viewport, and device scale factor across runs.
- Use deterministic test data where possible; hide timestamps or other intentionally changing regions when comparing baselines.
- Wait for fonts, images, and application data that affect the captured region.
- For protected sites, refresh expired sessions and verify the signed-in marker before capturing.
Playwright Test’s toHaveScreenshot() waits until two consecutive page screenshots are identical before comparing the last capture. That assertion is available through the Playwright test runner. It does not remove application-specific sources of nondeterminism.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. It can capture public URLs with one request; for a page that requires a private login session, browser automation may still be the right fit unless the page is accessible to the API using supported request credentials. Check the API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.
Troubleshooting authenticated captures
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot shows the login page | Login failed, state was saved too early, the session expired, or the capture used another context. | Assert a signed-in marker before saving state and before capture. Keep navigation and screenshot in the context with the session. |
| Playwright state file exists but is not signed in | The login flow redirected or created its session after the state was saved. | Wait for the final authenticated page condition, then save storageState. Check whether the app uses session storage, which needs custom handling. |
| Cookies are missing in Puppeteer | The capture uses a different browser context from the login flow. | Capture in the same context, or deliberately transfer the required cookies using the context cookie API. Confirm domain, path, and expiry are correct. |
| HTTP credentials do not log into the app | Page.authenticate() is for HTTP authentication, not form-based login. |
Automate the site’s login form or establish its normal browser session. |
| Page is blank or partly rendered | The screenshot ran before app data or assets were ready, or the site blocked the automated session. | Wait for a page-specific selector and inspect navigation and console errors. If a bot check appears, a screenshot cannot substitute for a valid authenticated session. |
| Element is absent or clipped | The selector is wrong, the element is conditionally rendered, or the page has not scrolled/loaded it. | Verify the locator against the signed-in page, wait for visibility, and scroll or load the relevant content before capture. |
| Visual baselines change between runs | Viewport, browser version, device scale, fonts, data, or dynamic content changed. | Pin the capture environment and test data; wait for required assets and mask known dynamic regions. |
| Authentication state was exposed | Saved state or credentials were committed, logged, or shared too broadly. | Remove the secret from repository history where needed, rotate the affected credentials, and keep auth files out of version control with restricted access. |
Performance, reliability, and cost
Do not select a library based on an assumed universal speed advantage. Capture time depends on the page, browser version, login path, network, asset loading, and waits. Puppeteer’s HTTP authentication enables request interception and may affect performance. Measure your own end-to-end flow with the same page, browser setup, and readiness condition if runtime matters.
For reliability, use a dedicated test account, verify authentication explicitly, and expect sessions to expire or require additional verification. Keep the login setup separate from capture assertions so failures identify whether the problem is authentication or rendering. For screenshot comparisons, stabilize inputs and use Playwright Test’s built-in screenshot assertion when its runner fits your project.
Both libraries require you to operate a browser and manage its execution environment. Include browser installation, compute, storage, retries, and maintenance in cost estimates. ScreenshotNeo offers a hosted alternative for URL captures: 1,000 shots monthly free without a card; paid tiers are Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. Only clean shots are billed; response headers identify the page verdict and billing status. Verify that its request model suits the page’s access requirements before substituting it for an authenticated browser context.
FAQ
Which should I pick for authenticated screenshots?
Start with Playwright if reusable saved browser state and integrated visual assertions fit the workflow. Puppeteer is a sound choice when it fits the existing browser automation code and you can manage the session setup there.
Can I save a login once and use it in both libraries?
Do not assume a state file or cookie format is directly interchangeable. Use each library’s documented context and state mechanisms, and verify the actual storage required by the site.
Does a screenshot API automatically access a private account?
No. A screenshot request does not inherently perform an arbitrary site’s interactive login. Confirm the API’s supported authentication options and security model, or use browser automation for the private session.
Does Playwright wait for a stable screenshot?
Playwright Test’s screenshot assertion waits for two consecutive captures to match before comparison. This is a test-runner feature, not a guarantee that an application has finished loading the content you care about.
