ScreenshotNeo

BlogGuides

Playwright BrowserContexts and Pages Explained

Learn how Playwright BrowserContexts and Pages work, when to use each, and how to handle tabs, popups, isolation, and cleanup.

By the ScreenshotNeo team1 October 20268 min read

Short answer: a BrowserContext is an isolated browser session, while a Page is one tab or popup inside that session. The hierarchy is Browser → BrowserContext → Page. Create another Page when tabs belong to the same signed-in user. Create another BrowserContext when you need a separate user, clean session, or isolated test.

Playwright describes contexts as independent, incognito-like profiles. Contexts do not share cookies or cache, and each context can contain multiple pages. The official Pages guide states: “Each BrowserContext can have multiple pages.” Read the Pages guide and the BrowserContext API reference.

BrowserContext vs Page at a glance

Need Use Reason
Open another tab for the same user context.newPage() Pages in one context share session state.
Test another user or a clean session browser.newContext() Contexts isolate cookies, cache, storage, and permissions.
Interact with a tab Page Navigation, locators, clicks, assertions, and screenshots are page operations.
Catch a popup from a known opener page.waitForEvent('popup') The event is tied to the page that opened it.
Observe any new tab in a context context.waitForEvent('page') The event covers pages created anywhere in that context.

Minimal setup

In Playwright Test, each test receives an isolated context and a default page fixture. With the Playwright library, create the browser, context, and page yourself:

import { chromium } from 'playwright';

const browser = await chromium.launch();
const context = await browser.newContext();
const page = await context.newPage();

await page.goto('https://example.com');
console.log(await page.title());

await context.close();
await browser.close();

The Browser API documents browser creation and context management. Closing a context closes all of its pages.

How BrowserContexts provide isolation

A context owns session-level state: cookies, local storage, session storage, permissions, emulation settings, and pages. Two contexts in one browser process remain separate.

import { chromium } from 'playwright';

const browser = await chromium.launch();
const alice = await browser.newContext();
const bob = await browser.newContext();

const alicePage = await alice.newPage();
const bobPage = await bob.newPage();

await alicePage.goto('https://example.com/login');
// Log Alice in; Bob's context cannot see Alice's cookies.
await bobPage.goto('https://example.com/account');

await alice.close();
await bob.close();
await browser.close();

Playwright’s isolation guide explains that “Playwright uses browser contexts to achieve Test Isolation.” See Browser contexts for the test-runner model.

Reuse state deliberately

Use storageState when a new context should start with a known login state. This copies a saved state into the new context; it does not make two contexts share changes made later.

const context = await browser.newContext({
  storageState: 'playwright/.auth/user.json'
});

Keep authentication files out of source control because they can contain cookies and tokens.

Pages: tabs, windows, and popups

A Page is the unit you navigate and interact with. A context can hold many pages:

const first = await context.newPage();
const second = await context.newPage();

console.log(context.pages().length); // 2
await second.goto('https://example.com');

Pages created in the same context inherit its viewport, user agent, locale, timezone, permissions, and other context settings.

Open a new tab yourself

const tab = await context.newPage();
await tab.goto('https://example.com/docs');

Capture a popup opened by a known page

Register the event wait before the click or JavaScript action that opens the popup. This prevents a race where the popup opens before the listener is attached.

const popupPromise = page.waitForEvent('popup');
await page.getByText('open the popup').click();
const popup = await popupPromise;
await popup.waitForLoadState();
console.log(await popup.url());

You can also subscribe continuously:

page.on('popup', async popup => {
  await popup.waitForLoadState();
  console.log('Popup:', popup.url());
});

The Page API documents the popup event and page lifecycle.

Observe any page created in a context

const pagePromise = context.waitForEvent('page');
await page.getByRole('button', { name: 'Open report' }).click();
const reportPage = await pagePromise;
await reportPage.waitForLoadState();

Use the context event when the opener is unknown or several pages may be created. Use the page event when you know exactly which page should produce the popup.

Configuring a BrowserContext

Context options establish defaults for every page in that session. Common options include:

Option Purpose
viewport Set page width and height.
deviceScaleFactor Emulate a retina-style pixel ratio.
userAgent Send a custom browser identity.
locale Set language and regional formatting.
timezoneId Emulate a timezone.
geolocation and permissions Provide location and grant access to it.
colorScheme Emulate light or dark preference.
extraHTTPHeaders Add headers to requests.
httpCredentials Supply HTTP basic-auth credentials.
storageState Start with saved cookies and storage.
baseURL Resolve relative URLs in navigation and locators.
proxy Route context traffic through a proxy where supported by your setup.
ignoreHTTPSErrors Allow invalid certificates in controlled test environments.
recordVideo, recordHar, and tracing Collect diagnostics; enable only when needed because they add I/O.
const context = await browser.newContext({
  viewport: { width: 1440, height: 900 },
  deviceScaleFactor: 2,
  locale: 'en-GB',
  timezoneId: 'Europe/London',
  colorScheme: 'dark',
  userAgent: 'MyTestBot/1.0',
  geolocation: { latitude: 51.5072, longitude: -0.1276 },
  permissions: ['geolocation'],
  extraHTTPHeaders: { 'X-Test-Run': 'context-1' },
  baseURL: 'https://example.com'
});

Check the current BrowserContext API for version-specific options and availability.

Context-level versus page-level settings

Prefer context settings for behavior shared by all tabs in a session. Use page methods for one tab’s navigation, DOM interaction, waiting, and screenshots. Some capabilities, such as route interception, can be configured at either level; a context route applies to every page in that context, while a page route applies only to one page.

await context.route('**/*.{png,jpg,jpeg}', route => route.abort());
await page.goto('https://example.com');

Be explicit about scope so a setting for one test tab does not unexpectedly affect another page.

Reliable page and popup workflows

  1. Create the browser once for a worker or process.
  2. Create a fresh context for each independent user or test.
  3. Create pages from that context.
  4. Register popup or page waits before the action that triggers them.
  5. Wait for a meaningful condition, such as a locator or response, instead of relying on arbitrary sleeps.
  6. Close the context in a finally block.
  7. Close the browser when the process is finished.
import { chromium } from 'playwright';

const browser = await chromium.launch();
const context = await browser.newContext();
try {
  const page = await context.newPage();
  await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
  await page.getByRole('heading').first().waitFor();
} finally {
  await context.close();
  await browser.close();
}

Complete popup example

import { chromium } from 'playwright';

const browser = await chromium.launch();
const context = await browser.newContext({ viewport: { width: 1280, height: 800 } });
const page = await context.newPage();

try {
  await page.goto('https://example.com');

  const popupPromise = page.waitForEvent('popup');
  await page.locator('a[target="_blank"]').first().click();
  const popup = await popupPromise;

  await popup.waitForLoadState('domcontentloaded');
  console.log({
    pagesInContext: context.pages().length,
    popupUrl: popup.url(),
    popupTitle: await popup.title()
  });
} finally {
  await context.close();
  await browser.close();
}

Playwright Test fixtures

With Playwright Test, the runner creates an isolated context and page fixture for each test by default:

import { test, expect } from '@playwright/test';

test('has a title', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveTitle(/Example/);
});

Create an additional page in the supplied context when the test needs another tab:

test('uses two tabs', async ({ context, page }) => {
  const docs = await context.newPage();
  await page.goto('https://example.com');
  await docs.goto('https://playwright.dev/docs/pages');
});

Create a separate context only when the test needs a second isolated identity. See the isolation documentation for fixture behavior.

Common errors and fixes

Error or symptom Cause Fix
Popup wait times out The listener was registered after the click, or no popup was opened. Create the promise before the action and verify the selector or event actually opens a new page.
Cookies appear in the wrong test Tests reused one context. Create a fresh context per isolated test or user.
New tab is missing from context.pages() The page was created in another context or the code checked too early. Use context.waitForEvent('page') and confirm the opener’s context.
Target page, context or browser has been closed Cleanup ran before an awaited operation completed. Await navigation and assertions before closing; keep cleanup in finally.
State is not shared between tabs The tabs belong to different contexts. Create both with the same context.
State unexpectedly persists A saved storageState or reused context carried it forward. Remove the state file for a clean run or create a new context without it.
Navigation hangs The page waits on long-running resources or an unreachable host. Set an appropriate timeout, use a narrower wait condition, and inspect network activity.
Click opens no popup Browser policy, a blocked event, or the site changed to same-tab navigation. Check page.url(), listen for context pages, and inspect the link target.
Geolocation is denied The context lacks a location or permission grant. Set both geolocation and permissions: ['geolocation'].

Performance, reliability, and cost

  • Reuse the browser process: launching a browser is expensive; create and close contexts for isolation while keeping the browser alive when practical.
  • Limit pages: each page consumes memory and may load network resources. Close pages you no longer need.
  • Use the narrowest wait: waiting for a specific locator or response is usually more deterministic than fixed delays.
  • Control diagnostics: tracing, video, HAR recording, and large downloads add disk and CPU work. Enable them for failed runs or targeted debugging.
  • Parallelism: separate contexts are a useful unit for parallel users, but set concurrency according to available CPU, memory, and the target site’s limits.
  • Cleanup: always close contexts and browsers so temporary profiles, pages, and connections are released.
  • Retries: retry only transient navigation or network failures. Repeating a state-changing action can create duplicate data.

Or skip the browser setup

If your goal is a clean website screenshot rather than browser automation, ScreenshotNeo provides a single HTTP request for PNG, JPEG, WebP, or PDF output. Its capture process accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot. Each step can be turned off.

Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed. The response reports the result with X-Page-Verdict and X-Billed headers. ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API documentation for all 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}`);

There are 1,000 screenshots per month free with no card. Paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

FAQ

Can one BrowserContext contain multiple pages?

Yes. Use context.newPage() and inspect them with context.pages().

Do pages in one context share cookies?

Yes. Pages in the same context use that context’s session state.

Do separate contexts share cache?

No. Contexts are isolated, incognito-like sessions.

Should I use a Page event or a Context event for popups?

Use the Page event when the opener is known. Use the Context event to observe any page created in the session.

What closes pages automatically?

Closing a BrowserContext closes all pages it owns. Closing the browser closes its remaining contexts.

Is a new Page the same as a new user session?

No. A new Page is another tab in the same session. A new BrowserContext is the boundary for a separate session.

Key takeaways

  • Think Browser → BrowserContext → Page.
  • Use pages for tabs that share identity and state.
  • Use contexts for separate users, clean tests, and isolation.
  • Register popup and page waits before the action that creates them.
  • Close contexts explicitly and browsers when the process ends.