Skip to main content
Slow automations increase compute costs and reduce throughput. This guide covers techniques to speed up your browser automations.
Use the trace viewer to identify which actions are taking the most time before optimizing.

Block unnecessary resources

Block heavy assets like images, stylesheets, and fonts that aren’t needed for your automation. This reduces page load time significantly.
TypeScript
import { BrowserContext, Page } from "playwright";

const BLOCKED_RESOURCE_TYPES = [
  'image',
  'stylesheet', 
  'font',
  'media',
  'texttrack',
  'beacon',
  'csp_report',
  'imageset'
];

export default async function automation(
  params: any,
  page: Page,
  context: BrowserContext
) {
  await page.route('**/*', (route) => {
    const request = route.request();
    
    if (BLOCKED_RESOURCE_TYPES.includes(request.resourceType())) {
      return route.abort();
    }
    
    return route.continue();
  });

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

Use network interception instead of DOM scraping

Intercept API responses directly instead of parsing the DOM. This is often 10x faster and more reliable. Use browser dev tools to find which API endpoints return the data you need. The following examples show the same scraper—first using DOM manipulation, then using network interception.
TypeScript
import { BrowserContext, Page } from "playwright";
import { goToUrl } from "@intuned/browser";

export default async function automation(
  params: any,
  page: Page,
  context: BrowserContext
) {
  await goToUrl({ page, url: "https://www.ycombinator.com/companies" });

  // Wait for companies to load
  await page.getByText("Loading companies...").waitFor({ state: "hidden" });
  await page.locator('a[href^="/companies/"]').first().waitFor();

  // Get all company card links
  const companyCards = page.locator('a[href^="/companies/"]:not([href="/companies"])');
  const count = await companyCards.count();

  const companies: { name: string; industry: string; tags: string[] }[] = [];

  for (let i = 0; i < count; i++) {
    const card = companyCards.nth(i);
    const href = await card.getAttribute("href");
    if (!href || href === "/companies") continue;

    const tagLinks = card.locator('a[href^="/companies?"]');
    const tagCount = await tagLinks.count();

    const tags: string[] = [];
    let industry = "";

    for (let j = 0; j < tagCount; j++) {
      const tagText = (await tagLinks.nth(j).innerText()).trim();
      const tagHref = await tagLinks.nth(j).getAttribute("href");

      if (tagHref?.includes("industry=") && !industry) {
        industry = tagText;
      }
      tags.push(tagText);
    }

    const slug = href.replace("/companies/", "");
    companies.push({ name: slug.replace(/-/g, " "), industry, tags });
  }

  return companies;
}
TypeScript
import { BrowserContext, Page } from "playwright";
import { goToUrl } from "@intuned/browser";

export default async function automation(
  params: any,
  page: Page,
  context: BrowserContext
) {
  const responsePromise = page.waitForResponse(async (response) => {
    const requestUrl = response.request().url();
    if (requestUrl.includes("indexes/*/queries")) {
      const jsonResponse = JSON.parse((await response.body()).toString());
      if (jsonResponse.results[0].hits.length > 1) {
        return true;
      }
    }
    return false;
  });

  await goToUrl({ page, url: "https://www.ycombinator.com/companies" });

  const response = await responsePromise;
  const jsonResponse = JSON.parse((await response.body()).toString());
  
  const companies = jsonResponse.results[0].hits.map((hit: any) => ({
    name: hit.name,
    website: hit.website,
    industry: hit.industry,
    tags: hit.tags,
  }));

  return companies;
}

Set shorter timeouts

The default Playwright timeout is 30 seconds. If your pages load quickly, reduce it to fail faster on errors.
TypeScript
import { BrowserContext, Page } from "playwright";

export default async function automation(
  params: any,
  page: Page,
  context: BrowserContext
) {
  // Set a shorter default timeout (in milliseconds)
  page.setDefaultTimeout(10000); // 10 seconds instead of 30
  
  await page.goto('https://example.com');
  await page.locator('#fast-loading-element').click();
  
  return {};
}

Optimize waiting strategies

Avoid waitForTimeout

Avoid waitForTimeout() with arbitrary delays—they waste time when pages load fast and fail when pages load slowly. Instead, wait for something specific.
// Wait for the table to appear
await page.locator('#data-table').waitForElementState("visible");

// Wait for at least one row to exist
await page.locator('.table-row').first().waitForElementState("visible");

Wait for DOM or network to settle

After actions that trigger page changes, use Intuned helpers:

Extract lists efficiently

When scraping lists, avoid iterating with locators—each locator call auto-waits, adding milliseconds per row that compounds over hundreds of elements. Instead:
  1. Wait for the list container to be visible
  2. Extract all data in a single evaluate() call using querySelectorAll
TypeScript
import { BrowserContext, Page } from "playwright";

export default async function automation(
  params: any,
  page: Page,
  context: BrowserContext
) {
  await page.goto('https://example.com/products');

  // Wait for the list container to be visible
  await page.locator('.product-list').waitFor({ state: 'visible' });

  // Extract all data in a single evaluate call
  const products = await page.evaluate(() => {
    return Array.from(document.querySelectorAll('.product-item')).map(el => ({
      name: el.querySelector('.name')?.textContent?.trim(),
      price: el.querySelector('.price')?.textContent?.trim(),
    }));
  });

  return { products };
}

Batch evaluate calls

Each evaluate() call is a round trip to the browser. Combine multiple queries into a single call.
TypeScript
import { BrowserContext, Page } from "playwright";

export default async function automation(
  params: any,
  page: Page,
  context: BrowserContext
) {
  await page.goto('https://example.com');
  
  // ❌ Bad: Multiple round trips
  // const title = await page.evaluate(() => document.title);
  // const url = await page.evaluate(() => window.location.href);
  // const count = await page.evaluate(() => document.querySelectorAll('.item').length);
  
  // ✅ Good: Single batched call
  const data = await page.evaluate(() => {
    return {
      title: document.title,
      url: window.location.href,
      count: document.querySelectorAll('.item').length,
      items: Array.from(document.querySelectorAll('.item')).map(el => ({
        text: el.textContent?.trim()
      }))
    };
  });
  
  return data;
}

Defer heavy processing

Move expensive operations outside the browser context. Extract raw data first, then process it after the automation completes. This includes image processing, LLM calls, data transformation, and file conversions.
TypeScript
import { BrowserContext, Page } from "playwright";

export default async function automation(
  params: any,
  page: Page,
  context: BrowserContext
) {
  await page.goto('https://example.com/products');
  
  // Extract raw data only - process later
  const rawData = await page.evaluate(() => {
    return Array.from(document.querySelectorAll('.product')).map(el => ({
      imageUrl: el.querySelector('img')?.src,
      title: el.querySelector('.title')?.textContent,
      price: el.querySelector('.price')?.textContent
    }));
  });
  
  return { rawData };
}

Use AuthSessions

Instead of logging in every run, use AuthSessions to reuse authenticated browser state. This can save 5-30 seconds per run depending on the login complexity.

Replace AI code with deterministic code

AI agents require multiple LLM calls per action. If your automation does predictable, repeatable steps, replace AI code with direct selectors. Use AI only for unpredictable page structures or as a fallback when deterministic code fails.
TypeScript
import z from "zod";
import type { BrowserContext, Page, Stagehand } from "@browserbasehq/stagehand";
import { attemptStore } from "@intuned/runtime";

interface Params {
  query: string;
}

export default async function handler(
  { query }: Params,
  page: Page,
  _: BrowserContext
) {
  const stagehand: Stagehand = attemptStore.get("stagehand");

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

  const agent = stagehand.agent({});

  // Each agent action requires multiple LLM calls
  await agent.execute({
    instruction: `Search for "${query}" and click on the first result.`,
  });

  const productSchema = z.object({
    product: z.object({
      name: z.string(),
      price: z.string(),
    }).nullable(),
  });

  return await page.extract({
    instruction: "Extract the product name and price",
    schema: productSchema,
  });
}
TypeScript
import { BrowserContext, Page } from "playwright";

interface Params {
  query: string;
}

export default async function automation(
  { query }: Params,
  page: Page,
  context: BrowserContext
) {
  await page.goto('https://example.com/products');
  
  // Direct selectors - instant execution
  await page.fill('#search-input', query);
  await page.click('button[type="submit"]');
  await page.locator('.product').first().click();
  
  // Direct extraction - no LLM needed
  const product = await page.evaluate(() => {
    return {
      name: document.querySelector('.product-name')?.textContent,
      price: document.querySelector('.product-price')?.textContent
    };
  });
  
  return { product };
}

Use fetch for static content

For static content, fetch HTML directly instead of using the browser. Only use the browser when JavaScript rendering is required.
TypeScript
import { BrowserContext, Page } from "playwright";

export default async function automation(
  params: any,
  page: Page,
  context: BrowserContext
) {
  const url = 'https://example.com/page';
  
  // Try fetch first
  const response = await fetch(url);
  const html = await response.text();
  
  // Check if data exists in static HTML
  if (html.includes('data-product-id')) {
    const matches = html.matchAll(/data-product-id="(\d+)"/g);
    const products = Array.from(matches).map(m => ({ id: m[1] }));
    return { products };
  }
  
  // Fallback to browser if JavaScript rendering is needed
  await page.goto(url);
  const products = await page.evaluate(() => {
    return Array.from(document.querySelectorAll('.product')).map(el => ({
      id: el.getAttribute('data-product-id'),
      name: el.textContent
    }));
  });
  
  return { products };
}

Adjust browser configuration

If optimizations don’t help and simple actions are still slow, the site may be JavaScript-heavy. Consider these configuration changes:
  • Use a larger machine — Upgrade your machine size in replication settings for resource-intensive sites.
  • Turn off unnecessary featuresHeadful mode, stealth mode, and proxies add overhead. Disable them in intuned.json if your automation works without them.

Additional tips

  • Build URLs directly — Instead of clicking through filters, build the final URL with query parameters (e.g., ?category=electronics&price=under-100).
  • Go to iframe URLs directly — Instead of using frameLocator, navigate directly to the iframe’s source URL to avoid loading the parent page.
  • Use fill() instead of pressSequentially()pressSequentially() types character-by-character. Use fill() for instant input unless you need keystroke events.
  • Avoid returning large data from evaluate() — Extract only the fields you need, not entire DOM elements or large HTML strings.