Why ‘page.goto()’ is slowing down your Playwright tests in 2026

Slow Playwright tests often come from page.goto() waiting longer than needed. Explore wait strategies, configurations, and optimization tips.

Written by Vinayak Mirani Vinayak Mirani
Reviewed by Nithya Mani Nithya Mani
Last updated: 17 August 2026 23 min read

Key Takeaways

  • page.goto() waits for a navigation state, so delays often come from the page, backend, network, redirects, or CI resources, not Playwright itself.
  • Use the lightest suitable waitUntil option, then wait for a specific element or response that proves the page is ready.
  • Measure navigation time before changing timeouts, blocking resources, or adding workers, because faster tests still need reliable assertions and meaningful end-to-end coverage.

At first glance, page.goto() seems simple. You give it a URL, Playwright opens the page, and the test continues. But when that one line starts taking several seconds or fails only in CI, it can slow down far more than one test.

The delay may come from the page, the network, the backend, or the wait condition used in the test. The browser may still be waiting for the load event. A slow API may be holding up the page. Third-party scripts may respond differently from one run to the next. Sometimes the page is fine and the CI runner is the actual problem.

Raising the timeout may stop the failure, but it does not fix the cause. You need to check what Playwright is waiting for and where the time is being spent. That is what helps you choose the right fix without making the test less reliable.

Understanding page.goto() in Playwright

The page.goto() method is a core function in Playwright used to navigate to a specified URL within a browser context during testing. When called, it directs the browser to load the page and, by default, waits until the entire page, including all dependent resources such as stylesheets, images, scripts, and iframes, is fully loaded and the load event is fired.

This comprehensive waiting ensures the page is completely ready for any interactions or assertions that follow in the test script. However, this default behavior can sometimes lead to slower test execution times, especially on resource-heavy pages.

Understanding how page.goto() manages navigation and loading helps in optimizing test performance effectively.

How page.goto() works in Playwright

page.goto() is typically one of the first actions in a test, as it opens the page or application you want to interact with.

Here’s a deeper look into how page.goto() works:

1. Navigates to a URL: The primary role of page.goto() is to navigate to the provided URL. For instance:

await page.goto(‘https://example.com’);

2. Waits for the page to load: By default, page.goto() will wait until the page load event is triggered. This means Playwright will wait for the browser to fully load all resources before continuing to the next step in the test.

3. Customizable Wait Mechanism: Playwright provides a waitUntil option that allows you to control what event or state should trigger the next action. You can specify different states for navigation, such as:

  • “load”: Waits for the full page to load, including all resources (default behavior).
  • “domcontentloaded”: Waits for the DOM to be fully parsed, but without waiting for images, styles, and scripts to load.
  • “networkidle”: Waits until there are no more than 2 network connections for at least 500 ms. This is useful for SPAs but may be overkill in traditional page tests.
  • “commit”: Waits until the navigation is committed, meaning the URL has changed, but doesn’t wait for the full load.

Example:

await page.goto(‘https://example.com’, { waitUntil: ‘domcontentloaded’ });

4. Timeout Handling: page.goto() also supports a timeout parameter that allows you to set a maximum amount of time to wait for the page to load. If the page doesn’t load within this time, a timeout error is thrown. This is especially useful in preventing tests from hanging indefinitely due to network or application issues.

await page.goto(‘https://example.com’, { timeout: 30000 }); // 30 seconds timeout

5. Browser Context: If you are running multiple tests, page.goto() can be used within different browser contexts or pages. This allows for isolated tests where each test runs in a clean session, ensuring that no state is shared between tests.

Common Reasons for page.goto() Slowdowns

A slow page.goto() does not always mean Playwright itself is slow. The method waits for a navigation condition, so the delay usually comes from the page, the application, the network, or the environment running the test.

Before changing timeouts, check which of these is holding up the navigation.

1. The page loads more resources than the test needs

Some pages start dozens of requests for images, fonts, analytics tools, chat widgets, advertisements, and embedded content. Your test may only need the login form, but the browser may still be loading resources that have nothing to do with that flow.

This becomes more noticeable when the page includes:

  • Large images or videos
  • Multiple JavaScript bundles
  • Third-party fonts
  • Embedded iframes
  • Tracking and analytics scripts

The page may look ready to you while Playwright is still waiting for the configured navigation event.

2. The selected waitUntil state is too strict

By default, page.goto() waits for the page’s load event. That can be reasonable for tests that depend on the complete page, but many tests do not need every resource to finish loading.

For example, a test that checks a heading or fills a form may only need the DOM to be available. Waiting for load adds time if images, widgets, or iframes are still loading.

networkidle can create an even longer delay on applications that keep making background requests. Polling, analytics calls, live updates, and service workers may prevent the page from becoming idle when you expect it to.

The right wait state depends on what the next test step actually needs.

3. The server is slow to return the document

Sometimes the delay happens before the browser starts rendering the page. A slow server response can keep page.goto() waiting even when the frontend is lightweight.

Possible causes include:

  • Slow database queries
  • Delayed authentication checks
  • Cold server instances
  • Overloaded test environments
  • Slow redirects
  • Middleware processing

This matters because changing waitUntil will not solve a slow initial response. The browser cannot parse or display the page until the server returns the document.

4. API calls delay page readiness

Many applications load the page shell first and then request data from APIs. The navigation may complete quickly, but the page may still need user details, product data, permissions, or configuration before it becomes usable.

If the application blocks rendering until those responses arrive, page.goto() may appear to be the problem even though an API is causing the delay.

This is common in dashboards, account pages, admin panels, and applications that make several requests during startup.

5. Third-party services respond unpredictably

Third-party scripts are outside your application’s control. An analytics service may respond quickly in one run and slowly in the next. A payment widget, map, advertisement, or support chat may also fail to load completely.

That can make navigation time inconsistent across test runs. It can also explain why a test passes locally but becomes slow or flaky in CI.

Before blocking a third-party resource, check whether the test depends on it. Blocking a payment script in a checkout test would hide a real integration problem. Blocking an unrelated analytics request may be reasonable.

6. Redirects add extra navigation time

A single page.goto() call may trigger more than one request. HTTP redirects, authentication redirects, locale detection, and trailing-slash rules can send the browser through several URLs before it reaches the final page.

Each redirect adds another server response and another opportunity for delay.

Unexpected redirects can also point to a configuration issue. For example, a test may open an HTTP URL that redirects to HTTPS, then redirect again to a login page because the expected session is missing.

7. The CI runner has limited resources

A test that takes two seconds locally may take much longer in CI. The application has not necessarily changed. The runner may simply have less CPU, memory, or network capacity.

Navigation slows down when the browser competes with:

  • Parallel workers
  • Build processes
  • Containers on the same machine
  • Video or trace recording
  • Other services started for the test run

Increasing the number of parallel workers can make the full suite faster, but it can also make each individual navigation slower when the machine is already under pressure.

Analyzing Your page.goto() Bottlenecks

To identify the causes of slow page.goto() executions in Playwright, it’s crucial to assess multiple factors that may be contributing to delays. Here’s how you can analyze and pinpoint the bottlenecks:

1. Measure Navigation Time

Log start and end times around page.goto() to understand how long navigation takes. This helps quantify the delay.

const start = Date.now();
await page.goto(‘https://example.com’);
const duration = Date.now() – start;
console.log(`Navigation took ${duration}ms`);

Use Playwright Trace Viewer for a detailed breakdown of navigation and resource loading.

2. Inspect Network Requests

Monitor network logs to identify slow or unnecessary requests (images, third-party scripts) that could be slowing down navigation.

page.on(‘response’, response => {
console.log(`Resource URL: ${response.url()} – Status: ${response.status()}`);
});
await page.goto(‘https://example.com’);

Block non-critical resources (like ads or trackers) to reduce load times.

await page.route(‘**/*.{jpg,png,css,js}’, route => route.abort());

3. Review waitUntil Settings

Adjust the waitUntil parameter to be more efficient. Instead of waiting for “load”, use “domcontentloaded” or “commit” to reduce waiting time for unnecessary resources.

await page.goto(‘https://example.com’, { waitUntil: ‘domcontentloaded’ });

4. Check for Backend/API Latency

Analyze the backend response time for APIs or databases that might slow down page load. Inspect network logs to see if any calls are causing delays.

5. Evaluate Test Environment

  • Assess the CI/CD environment’s resource limitations, as a constrained environment can slow down page navigation. Ensure that test environments (especially cloud or container setups) have sufficient resources.
  • Cold browser sessions (without cache) can slow down the test. Reuse sessions or browser contexts when possible.

6. Examine JavaScript or DOM Blocking

Identify if heavy JavaScript execution or rendering issues are delaying page readiness. Tools like Playwright’s performance insights can help you detect rendering or script bottlenecks.

By analyzing these aspects, you can isolate which part of the process is slowing down page.goto() and take the necessary steps to optimize navigation speed in your Playwright tests.

How to Speed Up page.goto() in Playwright Tests

A slow navigation needs a targeted fix. Changing every page.goto() call to domcontentloaded or blocking every image may reduce runtime, but it can also let tests continue before the page is usable.

Start with what the test needs immediately after navigation. Then remove only the waiting or network work that does not support that check.

1. Wait for the page state your test actually needs

page.goto() waits for the load event by default. That means the browser waits for resources such as images, stylesheets, scripts, and iframes that are part of the page load.

A test that checks a form or heading may not need all those resources. You can stop navigation at domcontentloaded, then use a web assertion for the element that shows the page is ready.

import { test, expect } from '@playwright/test';



test('opens the account page', async ({ page }) => {

  await page.goto('/account', {

    waitUntil: 'domcontentloaded',

  });



  await expect(

    page.getByRole('heading', { name: 'Your account' })

  ).toBeVisible();

});

Output –

01 wait for required page state

This separates two different checks. domcontentloaded confirms that the browser has parsed the document. The assertion confirms that the part needed by the test is available.

Do not change every navigation to commit just because it returns earlier. At commit, Playwright has received the response and started loading the document. The DOM may not be ready for the next test step. Use it only when the test has another clear readiness check after navigation.

2. Avoid using networkidle as a general readiness check

networkidle waits until there are no network connections for at least 500 milliseconds. Playwright discourages using it as the main readiness condition for tests.

Modern applications may continue to send analytics events, polling requests, notifications, or background updates after the visible page is ready. In those cases, network inactivity says little about whether the user can interact with the page.

Instead of waiting for the entire network to become quiet, wait for an application signal that matters to the test.

await page.goto('/orders', {

  waitUntil: 'domcontentloaded',

});



await expect(

  page.getByTestId('orders-table')

).toBeVisible();

For a page that loads data before showing its content, wait for the response and the visible result together.

const ordersResponse = page.waitForResponse(

  response =>

    response.url().includes('/api/orders') &&

    response.status() === 200

);



await page.goto('/orders', {

  waitUntil: 'domcontentloaded',

});



await ordersResponse;



await expect(

  page.getByTestId('orders-table')

).toBeVisible();

Output –

02 avoid networkidle readiness check

This gives the test a specific condition. It also makes failures easier to understand. You can see whether the API failed or whether the UI failed to render its response.

3. Remove waits that repeat work Playwright has already done

Navigation code often becomes slow because several waits are added over time. A test may wait for page.goto(), call waitForLoadState(), add a fixed delay, and then wait for a locator.

That sequence usually contains more waiting than the test needs.

await page.goto('/reports');

await page.waitForLoadState('load');

await page.waitForTimeout(3000);

await page.waitForSelector('#report-table');

A more focused version waits for the page and then checks the result that matters.

await page.goto('/reports', {

  waitUntil: 'domcontentloaded',

});



await expect(

  page.getByTestId('report-table')

).toBeVisible();

Output –

03 remove redundant waits

Playwright locators and assertions already wait for their conditions within the configured timeout. An additional waitForTimeout() only forces the test to pause for the full duration, even when the page becomes ready sooner.

waitForLoadState() is also unnecessary when page.goto() has already waited for the same load state. Playwright’s documentation notes that explicit load-state waits are not required in most cases because actions and assertions have their own waiting behaviour.

4. Block resources only when the test does not need them

Images, media files, and fonts can add network time to a page. Blocking them can reduce navigation time in tests that only verify application logic or text-based UI behaviour.

await page.route('**/*', async route => {

  const resourceType = route.request().resourceType();



  if (['image', 'media', 'font'].includes(resourceType)) {

    await route.abort();

    return;

  }



  await route.continue();

});



await page.goto('/search');

Output –

04 block unneeded resources

Do not apply this to visual tests, layout checks, font validation, image loading tests, or any flow where the blocked resource affects user behaviour.

Be careful with CSS and JavaScript. Blocking them may make the page load faster only because the real application is no longer running correctly. A test can pass against a broken version of the page.

There is another trade-off. Playwright disables the HTTP cache when request routing is enabled. A routing rule may save time by blocking large assets, but it may also prevent other resources from being served from the browser cache. Measure the result before adding routing across the full suite.

Service workers can also handle requests before a page-level route sees them. When request interception appears inconsistent, Playwright recommends setting serviceWorkers to block. Do that only when the service worker itself is not part of the behaviour under test.

import { defineConfig } from '@playwright/test';



export default defineConfig({

  use: {

    serviceWorkers: 'block',

  },

});

5. Mock slow APIs in tests that do not need the real backend

A UI test does not always need every backend service. If the test verifies how the page displays an empty recommendation list, waiting for a real recommendation service adds time and introduces another failure point.

You can return controlled data before opening the page.

await page.route('**/api/recommendations', async route => {

  await route.fulfill({

    status: 200,

    contentType: 'application/json',

    body: JSON.stringify({

      items: [],

    }),

  });

});



await page.goto('/products/123');



await expect(

  page.getByText('No recommendations available')

).toBeVisible();

Output –

05 routing caveats and mock slow apis

Use this in component-level or focused UI tests. Do not mock the API in a test intended to verify the complete browser-to-backend flow.

A healthy test suite usually needs both types. Focused tests use controlled responses for speed and predictable states. A smaller set of end-to-end tests keeps the real services in the path.

6. Reuse authentication state instead of repeating login navigation

Logging in through the UI before every test can add several navigations, redirects, and API calls. It also makes unrelated tests depend on the availability of the login flow.

Playwright can save authenticated browser state after one setup flow and load it into new browser contexts.

import { defineConfig } from '@playwright/test';



export default defineConfig({

  use: {

    baseURL: 'https://test.example.com',

    storageState: 'playwright/.auth/user.json',

  },

});

Output –

06 reuse authentication state

Each test still receives an isolated browser context, but it starts with the saved cookies and local storage. This avoids sharing one mutable context across unrelated tests.

Do not commit the authentication file to source control. It may contain cookies or headers that allow access to the test account. Add the authentication directory to .gitignore and recreate the state when it expires.

Tests that modify shared account data may need one account per parallel worker. Otherwise, two tests can change the same data at the same time and create failures that have nothing to do with navigation.

7. Open the route the test needs

Some tests always open the home page, wait for it to load, open a menu, and navigate to the actual page under test. That flow is necessary when the navigation menu is being tested. It is unnecessary setup when the test only verifies the profile page.

Instead of this:

await page.goto('/');

await page.getByRole('link', { name: 'Settings' }).click();

await page.getByRole('link', { name: 'Profile' }).click();

Open the target route directly:

await page.goto('/settings/profile');

Output –

07 open target route directly

Direct navigation removes intermediate page loads and unrelated UI interactions. It also keeps failures focused on the feature being tested.

Keep at least a few tests for menus, redirects, and user navigation paths. Direct URLs should reduce repeated setup, not replace coverage of the application’s navigation behaviour.

8. Check whether CI parallelism is slowing each navigation

More workers can reduce total suite time, but only while the CI machine has enough CPU and memory for them. When too many browsers run at once, each page may take longer to parse scripts, render content, and process network responses.

A lower worker count can sometimes finish the suite sooner because individual tests stop competing for the same resources.

import { defineConfig } from '@playwright/test';



export default defineConfig({

  workers: process.env.CI ? 2 : undefined,

});

Output –

08 ci parallelism workers

Playwright supports configuring the worker count through the test configuration or command line. The best number depends on the runner, the browser projects, and the services started alongside the tests.

Compare more than total duration. Check navigation time, CPU usage, memory pressure, retries, and failed workers. A fast run with frequent retries is not an improvement.

9. Keep timeouts as failure limits, not speed settings

Reducing a timeout does not make page.goto() complete sooner. It only makes Playwright stop waiting earlier.

A short navigation timeout can be useful when the application should respond quickly and a longer wait would only delay feedback. A longer timeout may be justified for a known slow environment. Neither setting fixes the underlying delay.

Set a navigation timeout that reflects the expected environment, then treat repeated timeout failures as evidence that something needs investigation.

import { defineConfig } from '@playwright/test';



export default defineConfig({

  use: {

    navigationTimeout: 30_000,

  },

});

Output –

09 navigation timeout failure limit

Playwright also allows a timeout on a single page.goto() call, but large per-test overrides can hide a broader performance problem.

The most reliable gains usually come from waiting for a specific page condition, removing repeated setup, and keeping slow external dependencies out of tests that do not need them. Make one change at a time and compare the navigation timing before applying it across the suite.

How These Optimizations Impact Your CI/CD Pipeline

A faster page.goto() call may save only a few seconds in one test. The effect becomes much larger when hundreds of tests open several pages during every pipeline run.

Still, navigation speed should not be judged only by total execution time. A change is useful only when the tests remain stable and continue to check the behaviour that matters.

1. Shorter execution time across the full suite

Suppose 300 tests each spend two unnecessary seconds waiting for a full page load. That adds ten minutes of browser time before retries and cross-browser runs are counted.

Reducing that wait can lower the duration of each worker. It can also shorten the full pipeline when navigation is a common setup step.

The gain depends on how the tests run. If the suite already has enough parallel workers, a two-second saving in every test may not remove the same amount from the pipeline clock. It still reduces compute usage across all workers.

Track both numbers:

  • Total pipeline duration
  • Combined browser execution time

The first affects developer feedback. The second affects infrastructure usage and cost.

2. Faster feedback after a code change

Developers often wait for browser tests before merging or deploying a change. Slow setup delays every result, even when the tested feature has nothing to do with page loading.

Direct navigation, saved authentication state, and focused readiness checks can help tests reach the feature sooner. A checkout test should spend most of its time checking checkout behaviour, not repeatedly opening the home page and logging in.

Faster results are useful only when failures remain clear. If a test stops waiting too early, it may fail later with an element-not-found error. That does not improve feedback because the error points to the wrong part of the flow.

A good navigation optimization should reduce waiting and keep the failure close to its real cause.

3. Lower pressure on CI workers

Each browser consumes CPU and memory while it loads scripts, processes styles, renders the page, and handles network activity. Several parallel workers can place heavy pressure on a small CI runner.

Blocking resources that a test does not need can reduce this load. Reusing authentication state can also remove repeated login pages, redirects, and API requests.

The effect is not always straightforward. Adding more workers may appear to increase parallelism, but it can make every navigation slower when the machine runs out of CPU or memory.

Watch for signs such as:

  • Navigation times that rise as worker count increases
  • Browser crashes or closed-page errors
  • Tests that pass locally but time out in CI
  • High memory usage during browser startup
  • More retries after increasing parallel execution

In these cases, lowering the worker count may produce a faster and more stable pipeline.

4. Fewer failures caused by shared infrastructure

External services can make navigation times vary between runs. Analytics tools, identity providers, payment widgets, and test APIs may respond quickly during one build and slowly during another.

You can mock services that are not part of the behaviour under test. This removes avoidable delays from focused UI tests.

Keep real integrations in the tests that are meant to verify them. For example, a test for an order summary can use controlled product data. A payment integration test should still use the real payment test environment.

This split helps the main suite run predictably without removing coverage of important integrations.

5. More useful timeout failures

Large navigation timeouts can keep a pipeline occupied for several minutes before a broken test finally fails. This is especially costly when the same problem affects several workers.

Timeouts should allow for normal variation in the CI environment. They should not hide repeated slowdowns.

Once navigation is measured and unnecessary waiting is removed, you can set limits that match expected behaviour. A page that normally opens in four seconds should not receive a two-minute timeout without a clear reason.

A sensible limit makes failures appear sooner and signals when application performance or test infrastructure has changed.

6. Better use of retries

Retries can help with occasional environmental failures, but they should not compensate for slow or unreliable navigation.

A navigation problem becomes expensive when it causes the test to fail near the beginning of the flow. The pipeline then repeats the entire test, including setup and browser startup.

Before increasing retries, check:

  • Whether the page is waiting for the wrong load state
  • Whether a background request prevents networkidle
  • Whether the CI runner has enough resources
  • Whether a third-party service is delaying the page
  • Whether the test opens pages that it does not need

Fixing the cause reduces both runtime and noise. It also makes retries more meaningful when a genuine temporary failure occurs.

7. Clearer capacity planning

Navigation measurements can help you decide how the suite should scale.

Track values such as median navigation time, 95th-percentile navigation time, retry rate, worker memory usage, and total browser minutes. Compare them before and after each change.

The median shows the usual experience. The 95th percentile exposes slower runs that may cause timeouts or flakiness. Looking at only the average can hide these outliers.

These measurements also help you separate test problems from application problems. If navigation becomes slower after a deployment but CI resources remain unchanged, the application may have introduced a slower redirect, document response, or startup request.

The strongest improvements reduce unnecessary browser work without weakening the test. That means shorter runs, fewer retries, clearer failures, and better use of CI capacity.

Conclusion

A slow page.goto() call usually points to something else in the test flow. The browser may be waiting for an unsuitable load state, a slow document response, background requests, repeated login steps, or an overloaded CI runner. Increasing the timeout may stop the immediate failure, but it will not remove the delay.

Start by checking where the navigation time is being spent. Then choose the smallest change that fits the test, such as using a more suitable waitUntil state, waiting for a specific UI condition, reusing authentication state, or mocking a service the test does not need.

Version History

  1. Aug 07, 2026 Current Version

    Reworked the selected sections with realistic test cases, deeper technical explanations, and actionable guidance. Repeated points were removed to keep the content focused.

    Nithya Mani
    Reviewed by Nithya Mani Lead Engineer
Tags
Automation Testing Real Device Cloud Website Testing
Vinayak Mirani
Vinayak Mirani

Lead Solution Engineer

Vinayak is a software engineer who has 5+ years working closely with customers on real engineering problems. He brings hands-on experience in diagnosing how software behaves across different environments and what it takes to fix it right.

Slow Navigation Delaying Tests?
Run optimized test suites across real browsers and devices.