Back to articles

Get the best of Playwright

Paul Klein
Paul KleinFounder & CEO
August 21, 2024
5 min read
Share

Playwright recently won the hearts of developers and became the most-used library to control headless browsers programmatically.

Its multi-language and multi-browser support makes it the perfect framework for various use cases, from testing to building automation and scraping scripts.

This article digs into its modern API with complete examples showcasing how to get the best of Playwright.

Copy link
Know your fundamentals

First, install Playwright with the latest tag to always benefit from the last available features:

npm i -S playwright-core@latest

Note: Use the _playwright-core_ package instead of the _playwright_ one to build automation or scraping scripts.

Now, let’s go over some of Playwright's essential concepts before moving forward with essential tips.

Copy link
Key concepts: Browser, BrowserContexts, Pages and Locators

Playwright enables you to interact with web pages through 4 main concepts: Browser, BrowserContexts, Pages, and Locators.

The Browser, BrowserContext, and Page objects represent different layers of the underlying Browser. In short, a Browser is identical to a regular Browser process; a BrowserContext is an isolated context with dedicated cookies and cache, while a Page is an actual Browser tab.

Locator enables developers to locate elements on a page in a finer and more stable way than regular CSS Selectors. We will cover how to build stable locators later in this article.

Here is how those 4 pillars concepts are commonly used in code:

TypeScript
import { chromium } from "playwright-core";

(async () => {
  // 1. Get a Browser instance
  const browser = await chromium.launch();

  // 2. Get a BrowserContext
  const defaultContext = browser.contexts()[0];

  // 3. Get a Page
  const page = defaultContext.pages()[0];

  // 4. Navigate
  await page.goto("https://browserbase.com");

  // 5. Locate an element
  const cta = page.getByRole('link').filter({ hasText: "We're hiring" });

  // 6. Act on the element
  cta.click();

  await page.close();
  await browser.close();
})();

Read our Understand Playwright’s BrowserContexts and Pages article to learn how to leverage each of them.

Copy link
The Auto-waiting mechanism

In addition to the key concepts, Playwright also brings an innovative approach to locators with the "Auto-waiting" functionality.

“Auto-waiting” is a feature embedded in Locator that guarantees that each action is performed on a “ready to interact” element. In this case, calling .click() on a Locator will check that:

  • The locator identifies exactly one DOM element.
  • The element is visible.
  • The element is stable.
  • The element receives events.
  • The element is enabled.

Below is an example of how that works:

JavaScript
import puppeteer from 'puppeteer';

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();

  await page.goto('https://developer.chrome.com/');

  await page.type('.devsite-search-field', 'automate beyond recorder');

  const searchResultSelector = '.devsite-result-item-link';
  await page.waitForSelector(searchResultSelector);
  await page.click(searchResultSelector);

  const textSelector = await page.waitForSelector('text/Customize and automate');
  const fullTitle = await textSelector?.evaluate(el => el.textContent);

  console.log('The title of this blog post is "%s".', fullTitle);

  await browser.close();
})();

As for Playwright, it removes the need to manually check readiness:

TypeScript
import { chromium } from "playwright-core";

(async () => {
  const browser = await chromium.launch();
  const defaultContext = browser.contexts()[0];
  const page = defaultContext.pages()[0];

  await page.locator('.devsite-search-field').pressSequentially(
    'automate beyond recorder',
    { delay: 25 }
  );

  await page.locator('.devsite-result-item-link').first().click();

  const textSelector = await page.locator('text/Customize and automate');
  await textSelector?.waitFor();
  const fullTitle = await textSelector?.innerText();

  console.log('The title of this blog post is "%s".', fullTitle);

  await browser.close();
})();

Copy link
Evaluating JavaScript (cautiously)

Playwright enables you to evaluate JavaScript in the page context:

TypeScript
import { chromium } from "playwright-core";

(async () => {
  const browser = await chromium.launch();
  const defaultContext = browser.contexts()[0];
  const page = defaultContext.pages()[0];

  await page.goto("https://browserbase.com");

  const href = await page.evaluate(() => document.location.href);
  console.log(href);

  await page.close();
  await browser.close();
})();

Running code within the browser is slow — use it only for:

  • Reading page-only values (window, globals)
  • Running authenticated API calls
  • Interacting with Chrome extensions
  • Command batching

Copy link
How to share variables with evaluation blocks

Example that fails:

TypeScript
import { chromium } from "playwright-core";

(async () => {
  const browser = await chromium.launch();
  const defaultContext = browser.contexts()[0];
  const page = defaultContext.pages()[0];

  await page.goto("https://www.browserbase.com");

  const two = 2;
  const sum = await page.evaluate(() => two + 2); // ❌ ReferenceError

  console.log(sum);
})();

Corrected using parameter binding:

TypeScript
const sum = await page.evaluate((two) => two + 2, two);

Copy link
How to load pages efficiently

Choosing the right loading strategy matters:

TypeScript
await page.goto('https://www.browserbase.com', {
  waitUntil: 'domcontentloaded',
});

Playwright supports four waitUntil options:

  • "commit" — raw HTML only
  • "domcontentloaded" — DOM ready
  • "load" — all resources loaded
  • "networkidle" — no network activity for 500 ms

Rule of thumb:
Use "domcontentloaded" for extraction and "load" for interaction.

Copy link
Favor Locators to Selectors

CSS selectors are fragile; Locators are stable.

CSS Selector:

TypeScript
page.locator('.episodes .episode:first-child button.buttonIcon.next-episode-button').click();

Locator equivalent:

TypeScript
getByRole('listitem')
  .first()
  .getByRole('button', { name: 'Next episode' })
  .click();

Copy link
Locator logical operators

TypeScript
const newEmail = page.getByRole('button', { name: 'New' });
const dialog = page.getByText('Confirm security settings');

await newEmail.or(dialog).toBeVisible();

if (await dialog.isVisible()) {
  await page.getByRole('button', { name: 'Dismiss' }).click();
}

await newEmail.click();

Copy link
Parent selection

TypeScript
const child = page.getByText('Hello');
const parent = page.getByRole('listitem').filter({ has: child });

Copy link
CSS pseudo-classes in locators

TypeScript
await page.locator('input:right-of(:text("Username"))').fill('value');

Copy link
Better performance with Route API, batching, and parallelization

Copy link
Batching commands

TypeScript
const buttonText = await page.evaluate(() => {
  const btn = document.querySelector('.myButton');
  btn.click();
  return btn.innerText;
});

Copy link
Filter out unnecessary resources

TypeScript
await page.route('https://events.framer.com/script', route => route.abort());

await page.route('**/*', route => {
  return route.request().resourceType() === 'image'
    ? route.abort()
    : route.continue();
});

Copy link
Parallel navigation

Multiple BrowserContext improve speed — but JS-heavy pages limit scalability.

Copy link
Using reliable headless browsers

Browserbase provides:

  • Enhanced Observability (Session Inspector)
  • Stealth Browser (anti-bot, proxies, captcha solving)
  • Advanced Capabilities (extensions, downloads, long sessions)
  • Flexible Integration
  • Robust Infrastructure

Start in under 5 minutes:
https://github.com/browserbase/quickstart-playwright-js