Testing Guide

Playwright BrowserContext: The Performance Feature Most Teams Overlook

When teams compare Playwright to Selenium, they focus on syntax and API design. The real differentiator is BrowserContext, a lightweight isolation primitive that unlocks faster parallel tests, cleaner fixtures, and reliable auto-waiting.

3-5x faster parallel test execution compared to Selenium multi-session setups

Switching from Selenium WebDriver instances to Playwright BrowserContexts cut our CI pipeline from 18 minutes to 5.

1. What Is BrowserContext and Why Does It Matter?

A BrowserContext in Playwright is an isolated browser session that runs inside a shared browser instance. Think of it as an incognito window: it has its own cookies, local storage, session storage, and cache, completely separate from other contexts. But unlike launching a whole new browser process, creating a context is nearly instantaneous.

This matters because browser startup is the single biggest overhead in end-to-end testing. In traditional Selenium setups, running tests in parallel means launching multiple browser processes, each consuming hundreds of megabytes of memory and taking seconds to initialize. Playwright launches one browser and creates as many contexts as needed, each one lightweight and isolated.

The architecture is straightforward: a single browser process serves multiple contexts. Each context can have multiple pages (tabs). Tests interact with pages, and the context boundary ensures they never leak state to each other. When a test finishes, its context is disposed, and all associated resources are cleaned up automatically.

2. BrowserContext vs. Selenium Sessions

In Selenium, the unit of isolation is the WebDriver instance. Each WebDriver controls one browser process. If you want to run 10 tests in parallel with full isolation, you need 10 browser processes. On a CI machine with 4 GB of RAM, that means you might cap out at 4 or 5 parallel Chrome instances before memory pressure starts causing failures.

Playwright's BrowserContext changes this equation entirely. One browser process can host dozens of contexts. Each context uses a fraction of the memory a full browser process would require. In practice, teams report running 10 to 20 parallel workers on the same hardware that struggled with 4 Selenium instances.

Session Management Differences

Selenium's session model creates another subtle problem: cookie and storage management. Clearing cookies between tests in Selenium requires explicit driver.manage().deleteAllCookies() calls, and even then, some storage mechanisms (like IndexedDB or Service Workers) can persist. Teams often resort to restarting the entire browser between tests to guarantee a clean slate, which destroys any performance gains from reuse.

With BrowserContext, you get a fresh session by default. No manual cleanup is needed. Create a context, run the test, dispose the context. The browser process stays warm while the test environment is perfectly clean. This is the kind of design decision that doesn't show up in feature comparison tables but makes an enormous difference in practice.

Try Assrt for free

Open-source AI testing framework. No signup required.

Get Started

3. Isolated Contexts for Parallel Testing

Playwright's test runner parallelizes at the worker level by default. Each worker gets its own browser process, and within that process, each test gets its own BrowserContext. This two-level isolation means tests never share state, even when running simultaneously on the same machine.

The configuration is simple. In your playwright.config.ts, set the workers property to control how many parallel workers to use. On CI, you might set it to the number of CPU cores. Locally, you might limit it to half your cores to keep your machine responsive. Playwright automatically distributes test files across workers and manages context lifecycle.

Handling Shared Resources

The one area that requires attention is shared backend resources. If all your parallel tests hit the same database, you need a strategy for data isolation. Common approaches include using unique identifiers per test (appending the worker index or a UUID to test data), running each worker against a separate database schema, or using API-based setup and teardown that creates and destroys test data atomically.

Playwright exposes testInfo.parallelIndex in each test, giving you a stable worker identifier you can use to partition data. Combined with storageState for authentication, you can have each worker operate as a different user against a shared backend, achieving true end-to-end isolation without complex infrastructure.

4. Fixture Patterns That Leverage BrowserContext

Playwright's fixture system is one of its most powerful and underused features. Fixtures let you define reusable setup and teardown logic that integrates directly with the test runner. The built-in page and context fixtures already give you a fresh BrowserContext and Page for each test, but you can extend them for your specific needs.

Custom Fixture Examples

Authenticated context fixture: Create a fixture that loads a pre-saved storageState file, giving every test an already-authenticated session. The storage state file is generated once during global setup (by actually logging in through the UI) and reused across all tests. This eliminates the login flow from every test without sacrificing isolation.

Multi-role fixture: For applications with role-based access, create fixtures like adminPage, editorPage, and viewerPage. Each fixture creates a BrowserContext with the appropriate storage state. Tests that need to verify role-based behavior can request multiple fixtures and interact with both simultaneously, something that would require managing multiple WebDriver instances in Selenium.

Seeded data fixture: A fixture that calls your API to create test data before the test and clean it up after. Because fixtures compose naturally, you can combine an authenticated context with seeded data: the test gets a logged-in page with the exact data it needs, created fresh and cleaned up automatically.

Tools like Assrt complement these patterns well. As an open-source AI-powered test automation framework, Assrt can auto-discover test scenarios from your running application and generate Playwright tests that follow fixture best practices. Its self-healing selectors also reduce the maintenance burden of keeping locators up to date as your UI evolves.

5. Auto-Waiting vs. Explicit Waits

If you've written Selenium tests, you know the pain of WebDriverWait and ExpectedConditions. Every interaction needs an explicit wait: wait for the element to be visible, wait for it to be clickable, wait for the page to load. Miss a wait, and your test fails intermittently. Add too many waits, and your test suite crawls.

Playwright's auto-waiting eliminates most of this. When you call page.click('button'), Playwright automatically waits for the element to be attached to the DOM, visible, stable (not animating), enabled, and not obscured by other elements. It does this with actionability checks that run in a tight loop, typically resolving in milliseconds when the element is ready.

This also means you rarely need page.waitForTimeout() (the equivalent of Thread.sleep()in Selenium). Hard-coded timeouts are the leading cause of both slow tests and flaky tests. They're too long when the page is fast and too short when CI is under load. Auto-waiting adapts to actual page readiness.

When You Still Need Explicit Waits

Auto-waiting covers element interactions, but there are cases where you need to wait for something other than an element. For example, waiting for a network request to complete (page.waitForResponse()), waiting for a URL change (page.waitForURL()), or waiting for a specific application state that isn't reflected in the DOM. These explicit waits are event-driven, not time-based, so they resolve as soon as the condition is met.

6. Migration Tips from Selenium Habits

Migrating from Selenium to Playwright is less about rewriting tests and more about unlearning patterns that Selenium forced you to adopt. Here are the most common habits to break:

Stop managing browser lifecycle manually. In Selenium, you're responsible for creating WebDriver instances, managing their lifecycle, and ensuring they're properly closed. Playwright's test runner handles all of this through fixtures. Let the framework manage browsers, contexts, and pages. Your tests should only interact with the page fixture.

Remove all sleep calls. Go through your migrated tests and remove every page.waitForTimeout() call. Replace them with specific waiters (waitForResponse, waitForURL) or simply remove them. Playwright's auto-waiting will handle the rest. If a test fails without the timeout, that indicates a real issue worth investigating, not a timing problem to paper over.

Replace XPath and CSS selectors with Playwright locators. Selenium tests tend to accumulate complex XPath expressions and fragile CSS selectors. When migrating, convert these to Playwright's built-in locator methods: getByRole(), getByText(), getByLabel(), and getByTestId(). This is also a good opportunity to audit your locators for stability and add data-testid attributes where needed.

Embrace parallel execution from day one. Many Selenium suites run sequentially because isolation was never enforced. When migrating, take the opportunity to fix shared state issues and run tests in parallel from the start. The CI time savings compound quickly. If you have tests that are difficult to isolate, tools like Assrt (an open-source, free testing framework) can help by generating new Playwright tests with proper isolation built in, alongside visual regression checks that catch unexpected UI changes.

The shift from Selenium to Playwright is ultimately about trusting the framework to handle the low-level concerns (waiting, browser management, session isolation) so you can focus on what matters: writing tests that verify your application works correctly for real users.

Ready to automate your testing?

Assrt discovers test scenarios, writes Playwright tests from plain English, and self-heals when your UI changes.

$npm install @assrt/sdk