
Get the best of Playwright
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 linkKnow 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 linkKey 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:
TypeScriptimport { 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 linkThe 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:
JavaScriptimport 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:
TypeScriptimport { 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 linkEvaluating JavaScript (cautiously)
Playwright enables you to evaluate JavaScript in the page context:
TypeScriptimport { 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 linkHow to share variables with evaluation blocks
Example that fails:
TypeScriptimport { 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:
TypeScriptconst sum = await page.evaluate((two) => two + 2, two);
Copy linkHow to load pages efficiently
Choosing the right loading strategy matters:
TypeScriptawait 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 linkFavor Locators to Selectors
CSS selectors are fragile; Locators are stable.
CSS Selector:
TypeScriptpage.locator('.episodes .episode:first-child button.buttonIcon.next-episode-button').click();
Locator equivalent:
TypeScriptgetByRole('listitem') .first() .getByRole('button', { name: 'Next episode' }) .click();
Copy linkLocator logical operators
TypeScriptconst 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 linkParent selection
TypeScriptconst child = page.getByText('Hello'); const parent = page.getByRole('listitem').filter({ has: child });
Copy linkCSS pseudo-classes in locators
TypeScriptawait page.locator('input:right-of(:text("Username"))').fill('value');
Copy linkBetter performance with Route API, batching, and parallelization
Copy linkBatching commands
TypeScriptconst buttonText = await page.evaluate(() => { const btn = document.querySelector('.myButton'); btn.click(); return btn.innerText; });
Copy linkFilter out unnecessary resources
TypeScriptawait page.route('https://events.framer.com/script', route => route.abort()); await page.route('**/*', route => { return route.request().resourceType() === 'image' ? route.abort() : route.continue(); });
Copy linkParallel navigation
Multiple BrowserContext improve speed — but JS-heavy pages limit scalability.
Copy linkUsing 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
