> ## Documentation Index
> Fetch the complete documentation index at: https://intunedhq.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# How to make automations faster

Slow automations increase compute costs and reduce throughput. This guide covers techniques to speed up your browser automations.

<Tip>
  Use the [trace viewer](/main/02-features/observability-monitoring-logs#inspect-run-attempts) to identify which actions are taking the most time before optimizing.
</Tip>

## Block unnecessary resources

Block heavy assets like images, stylesheets, and fonts that aren't needed for your automation. This reduces page load time significantly.

<Accordion title="Example">
  <CodeGroup dropdown>
    ```typescript TypeScript theme={null}
    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 {};
    }
    ```

    ```python Python theme={null}
    from playwright.async_api import Page
    from typing import Any, Dict

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


    async def automation(
        page: Page,
        params: Dict[str, Any] | None = None,
        **kwargs
    ):
        async def handle_route(route):
            request = route.request
            
            if request.resource_type in BLOCKED_RESOURCE_TYPES:
                await route.abort()
            else:
                await route.continue_()
        
        await page.route('**/*', handle_route)
        await page.goto('https://example.com')
        
        return {}
    ```
  </CodeGroup>
</Accordion>

## 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.

<Accordion title="DOM Manipulation (Slower)">
  <CodeGroup dropdown>
    ```typescript TypeScript theme={null}
    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;
    }
    ```

    ```python Python theme={null}
    from playwright.async_api import Page
    from typing import Any, Dict
    from intuned_browser import go_to_url


    async def automation(
        page: Page,
        params: Dict[str, Any] | None = None,
        **kwargs
    ):
        await go_to_url(page=page, url="https://www.ycombinator.com/companies")

        # Wait for companies to load
        await page.get_by_text("Loading companies...").wait_for(state="hidden")
        await page.locator('a[href^="/companies/"]').first.wait_for()

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

        companies = []

        for i in range(count):
            card = company_cards.nth(i)
            href = await card.get_attribute("href")
            if not href or href == "/companies":
                continue

            tag_links = card.locator('a[href^="/companies?"]')
            tag_count = await tag_links.count()

            tags = []
            industry = ""

            for j in range(tag_count):
                tag_text = (await tag_links.nth(j).inner_text()).strip()
                tag_href = await tag_links.nth(j).get_attribute("href")

                if tag_href and "industry=" in tag_href and not industry:
                    industry = tag_text
                tags.append(tag_text)

            slug = href.replace("/companies/", "")
            companies.append({"name": slug.replace("-", " "), "industry": industry, "tags": tags})

        return companies
    ```
  </CodeGroup>
</Accordion>

<Accordion title="Network Interception (Faster)">
  <CodeGroup dropdown>
    ```typescript TypeScript theme={null}
    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;
    }
    ```

    ```python Python theme={null}
    from playwright.async_api import Page
    from typing import Any, Dict
    from intuned_browser import go_to_url
    import json


    async def automation(
        page: Page,
        params: Dict[str, Any] | None = None,
        **kwargs
    ):
        async def match_response(response):
            if "indexes/*/queries" in response.request.url:
                body = await response.body()
                json_response = json.loads(body.decode())
                if len(json_response["results"][0]["hits"]) > 1:
                    return True
            return False

        response_promise = page.wait_for_response(match_response)

        await go_to_url(page=page, url="https://www.ycombinator.com/companies")

        response = await response_promise
        body = await response.body()
        json_response = json.loads(body.decode())

        companies = [
            {
                "name": hit["name"],
                "website": hit["website"],
                "industry": hit["industry"],
                "tags": hit["tags"],
            }
            for hit in json_response["results"][0]["hits"]
        ]

        return companies
    ```
  </CodeGroup>
</Accordion>

## Set shorter timeouts

The default Playwright timeout is 30 seconds. If your pages load quickly, reduce it to fail faster on errors.

<Accordion title="Example">
  <CodeGroup dropdown>
    ```typescript TypeScript theme={null}
    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 {};
    }
    ```

    ```python Python theme={null}
    from playwright.async_api import Page
    from typing import Any, Dict


    async def automation(
        page: Page,
        params: Dict[str, Any] | None = None,
        **kwargs
    ):
        # Set a shorter default timeout (in milliseconds)
        page.set_default_timeout(10000)  # 10 seconds instead of 30
        
        await page.goto('https://example.com')
        await page.locator('#fast-loading-element').click()
        
        return {}
    ```
  </CodeGroup>
</Accordion>

## 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.

```typescript theme={null}
// 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:

* [waitForDomSettled](/automation-sdks/intuned-sdk/typescript/helpers/functions/waitForDomSettled) — After clicking buttons that modify the page
* [withNetworkSettledWait](/automation-sdks/intuned-sdk/typescript/helpers/functions/withNetworkSettledWait) — After navigation or actions that trigger API calls

### 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`

<Accordion title="Example: Scraping a list">
  <CodeGroup dropdown>
    ```typescript TypeScript theme={null}
    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 };
    }
    ```

    ```python Python theme={null}
    from playwright.async_api import Page
    from typing import Any, Dict


    async def automation(
        page: Page,
        params: Dict[str, Any] | None = None,
        **kwargs
    ):
        await page.goto('https://example.com/products')

        # Wait for the list container to be visible
        await page.locator('.product-list').wait_for(state='visible')

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

        return {"products": products}
    ```
  </CodeGroup>
</Accordion>

## Batch evaluate calls

Each `evaluate()` call is a round trip to the browser. Combine multiple queries into a single call.

<Accordion title="Example">
  <CodeGroup dropdown>
    ```typescript TypeScript theme={null}
    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;
    }
    ```

    ```python Python theme={null}
    from playwright.async_api import Page
    from typing import Any, Dict


    async def automation(
        page: Page,
        params: Dict[str, Any] | None = None,
        **kwargs
    ):
        await page.goto('https://example.com')
        
        # ❌ Bad: Multiple round trips
        # title = await page.evaluate('() => document.title')
        # url = await page.evaluate('() => window.location.href')
        # count = await page.evaluate('() => document.querySelectorAll(".item").length')
        
        # ✅ Good: Single batched call
        data = await page.evaluate("""
            () => ({
                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
    ```
  </CodeGroup>
</Accordion>

## 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.

<Accordion title="Example">
  <CodeGroup dropdown>
    ```typescript TypeScript theme={null}
    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 };
    }
    ```

    ```python Python theme={null}
    from playwright.async_api import Page
    from typing import Any, Dict


    async def automation(
        page: Page,
        params: Dict[str, Any] | None = None,
        **kwargs
    ):
        await page.goto('https://example.com/products')
        
        # Extract raw data only - process later
        raw_data = await page.evaluate("""
            () => Array.from(document.querySelectorAll('.product')).map(el => ({
                imageUrl: el.querySelector('img')?.src,
                title: el.querySelector('.title')?.textContent,
                price: el.querySelector('.price')?.textContent
            }))
        """)
        
        return {"rawData": raw_data}
    ```
  </CodeGroup>
</Accordion>

## Use AuthSessions

Instead of logging in every run, use [AuthSessions](/main/02-features/auth-sessions) 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.

<Accordion title="Stagehand Agent (Slower)">
  <CodeGroup dropdown>
    ```typescript TypeScript theme={null}
    import z from "zod";
    import { Stagehand } from "@browserbasehq/stagehand";
    import type { BrowserContext, Page } from "playwright";
    import { attemptStore, getAiGatewayConfig } from "@intuned/runtime";

    interface Params {
      query: string;
    }

    async function getWebSocketUrl(cdpUrl: string): Promise<string> {
      if (cdpUrl.includes("ws://") || cdpUrl.includes("wss://")) {
        return cdpUrl;
      }

      const versionUrl = cdpUrl.endsWith("/")
        ? `${cdpUrl}json/version`
        : `${cdpUrl}/json/version`;
      const response = await fetch(versionUrl);
      const data = await response.json();
      return data.webSocketDebuggerUrl;
    }

    export default async function automation(
      { query }: Params,
      page: Page,
      _context: BrowserContext
    ) {
      const { baseUrl, apiKey } = await getAiGatewayConfig();
      const cdpUrl = attemptStore.get("cdpUrl") as string;
      const webSocketUrl = await getWebSocketUrl(cdpUrl);

      const stagehand = new Stagehand({
        env: "LOCAL",
        localBrowserLaunchOptions: {
          cdpUrl: webSocketUrl,
          viewport: { width: 1280, height: 800 },
        },
        model: {
          modelName: "openai/gpt-5-mini",
          apiKey,
          baseURL: baseUrl,
        },
      });
      await stagehand.init();

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

        // Each Stagehand action still requires LLM calls.
        await stagehand.act(`Search for "${query}" and select the first result.`);

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

        return await stagehand.extract(
          "Extract the product name and price.",
          productSchema
        );
      } finally {
        await stagehand.close();
      }
    }
    ```

    ```python Python theme={null}
    from typing import TypedDict

    from intuned_runtime import attempt_store, get_ai_gateway_config
    from playwright.async_api import Page
    from pydantic import BaseModel
    from stagehand import AsyncStagehand
    from stagehand.types.model_config_param import ModelConfigParam
    from stagehand.types.session_start_params import Browser, BrowserLaunchOptions


    class Params(TypedDict):
        query: str


    class Product(BaseModel):
        name: str
        price: str


    class ProductResponse(BaseModel):
        product: Product | None


    async def automation(page: Page, params: Params, **_kwargs):
        query = params["query"]
        base_url, api_key = get_ai_gateway_config()
        cdp_url = attempt_store.get("cdp_url")

        model_name = "openai/gpt-5-mini"
        model_config: ModelConfigParam = {
            "model_name": model_name,
            "api_key": api_key,
            "base_url": base_url,
            "provider": "openai",
        }

        client = AsyncStagehand(
            server="local",
            model_api_key=api_key,
            local_ready_timeout_s=30.0,
        )
        launch_options: BrowserLaunchOptions = {"headless": False}
        if cdp_url is not None:
            launch_options["cdp_url"] = str(cdp_url)

        browser: Browser = {"type": "local", "launch_options": launch_options}
        session = await client.sessions.start(model_name=model_name, browser=browser)
        session_id = session.data.session_id

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

            # Each Stagehand action still requires LLM calls.
            await client.sessions.act(
                id=session_id,
                input=f'Search for "{query}" and select the first result.',
                options={"model": model_config},
            )

            result = await client.sessions.extract(
                id=session_id,
                instruction="Extract the product name and price.",
                options={"model": model_config},
                schema={
                    "type": "object",
                    "properties": {
                        "product": {
                            "type": ["object", "null"],
                            "properties": {
                                "name": {"type": "string"},
                                "price": {"type": "string"},
                            },
                            "required": ["name", "price"],
                        }
                    },
                    "required": ["product"],
                },
            )

            return ProductResponse.model_validate(result.data.result)
        finally:
            await client.sessions.end(session_id)
    ```
  </CodeGroup>
</Accordion>

<Accordion title="Deterministic Code (Faster)">
  <CodeGroup dropdown>
    ```typescript TypeScript theme={null}
    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 };
    }
    ```

    ```python Python theme={null}
    from playwright.async_api import Page
    from typing import Any, Dict


    async def automation(
        page: Page,
        params: Dict[str, Any] | None = None,
        **kwargs
    ):
        query = params.get("query", "")
        
        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
        product = await page.evaluate("""
            () => ({
                name: document.querySelector('.product-name')?.textContent,
                price: document.querySelector('.product-price')?.textContent
            })
        """)
        
        return {"product": product}
    ```
  </CodeGroup>
</Accordion>

## 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.

<Accordion title="Example">
  <CodeGroup dropdown>
    ```typescript TypeScript theme={null}
    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 };
    }
    ```

    ```python Python theme={null}
    from playwright.async_api import Page
    from typing import Any, Dict
    import httpx
    import re


    async def automation(
        page: Page,
        params: Dict[str, Any] | None = None,
        **kwargs
    ):
        url = 'https://example.com/page'
        
        # Try fetch first
        async with httpx.AsyncClient() as client:
            response = await client.get(url)
            html = response.text
        
        # Check if data exists in static HTML
        if 'data-product-id' in html:
            matches = re.findall(r'data-product-id="(\d+)"', html)
            products = [{"id": match} for match in matches]
            return {"products": products}
        
        # Fallback to browser if JavaScript rendering is needed
        await page.goto(url)
        products = await page.evaluate("""
            () => Array.from(document.querySelectorAll('.product')).map(el => ({
                id: el.getAttribute('data-product-id'),
                name: el.textContent
            }))
        """)
        
        return {"products": products}
    ```
  </CodeGroup>
</Accordion>

## 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](/main/05-references/intuned-json#replication) for resource-intensive sites.
* **Turn off unnecessary features** — [Headful mode](/main/02-features/stealth-mode-captcha-solving-proxies#headful-mode), [stealth mode](/main/02-features/stealth-mode-captcha-solving-proxies#stealth-mode), and [proxies](/main/02-features/stealth-mode-captcha-solving-proxies#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.

## Related resources

<CardGroup cols={2}>
  <Card title="Optimize cost" icon="dollar-sign" href="/main/03-how-to/solve/optimize-cost">
    Reduce compute and AI costs
  </Card>

  <Card title="Observability and logs" icon="chart-line" href="/main/02-features/observability-monitoring-logs">
    Debug with trace viewer
  </Card>

  <Card title="AuthSessions" icon="key" href="/main/02-features/auth-sessions">
    Reuse authenticated browser state
  </Card>
</CardGroup>
