# Why "page.goto()" is slowing down your Playwright tests

> Learn how to optimize Playwright tests by adjusting page.goto() configurations to speed up your tests and reflect actual user behavior.

Source: https://www.checklyhq.com/blog/why-page-goto-is-slowing-down-your-playwright-test/

---

[Blog](https://www.checklyhq.com/blog/) / [Playwright](https://www.checklyhq.com/blog/tag/playwright/) / [Synthetic Monitoring](https://www.checklyhq.com/blog/tag/synthetic-monitoring/)

# Why "page.goto()" is slowing down your Playwright tests

[Stefan Judis](https://www.checklyhq.com/blog/author/stefan/)

July 10, 2024 · Updated November 22, 2024

When you invest time and effort into creating a well-running end-to-end test suite or adopt [Playwright synthetic monitoring](https://www.checklyhq.com/blog/[uid]/) with Checkly, you should focus on two things.

Your tests must be stable because few things are worse than an unreliable test suite. But also, your tests must be fast because no one wants to wait hours to receive the green light when you're on the edge of your seat to deploy this critical production hotfix.

What if I told you you’re slowing down your tests with the most basic Playwright action ever — `page.goto()`?

Check out this video or read on to learn more. Both include examples and ways to speed up your Playwright tests.

Ready? Let’s go!

## The start of every Playwright script — `page.goto()`

Here’s a pretty basic Playwright script.

login.spec.js

```
import { expect, test } from "@playwright/test";

test("Login works", async ({ page }) => {
await page.goto("/");

const loginLink = page.getByRole("link", { name: "Login" });
await loginLink.click();

const loginHeader = page.getByRole("heading", { name: "Login" });
await expect(loginHeader).toBeVisible();

// More things to check after the login link was clicked
// ...
});
```

It navigates to the root of a `baseURL` defined in the `playwright.config`. It locates a link, clicks it, and then tests whether the site’s UI has been updated accordingly (I’ve hidden most of the actions and assertions because they don’t matter for this post).

All this works great, and I’m sure you’ve written similar Playwright code before. If you inspect your tests and their duration, you might have discovered that `page.goto()` execution times can vary greatly.

Let me show you an example.

In the Playwright trace above, `page.goto()` takes ten seconds! Surprisingly, the film strip on top of the trace viewer shows that the example site was visible for almost the entire waiting time.

And this is a single test for a super-speedy local e-commerce site. Testing all the core functionality of a reasonably sized shop could easily include a hundred tests, and then **such a small delay results in more than 15 minutes of waiting time in your CI/CD pipeline**!

_(Of course, you can shorten this waiting time by parallelizing your tests, but the argument of unnecessary waiting still holds.)_

What are we waiting for?

## `page.goto()` and the `load` event

If you check [the Playwright docs](https://playwright.dev/docs/next/api/class-page#page-goto), you’ll discover that, by default, `page.goto()` waits for the `load` event.

```
await page.goto("/");

// is the same as

await page.goto("/", {waitUntil: "load"});
```

What’s the `load` event? [A quick peek at MDN gives us the answer](https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event).

> The load event is fired when the whole page has loaded, including all dependent resources such as stylesheets, scripts, iframes, and images.

Let’s think this through. Whenever you call `page.goto()` you’re waiting for all stylesheets,scripts, iframes, and images to be loaded. In summary, **you’re waiting for everything(!) to be loaded** before running your test.

When inspecting the network tab of my example trace file, I found an SVG image delaying all the test actions. Why is this file so slow? I don’t know, but there’s always a chance of resources taking longer to load.

Now, this is only a small demo application. Consider a full-blown e-commerce shop; there will be hundreds of images, let alone all the scripts coming from “somewhere”. For such a site, the `page.goto()` default configuration will wait for all the resources to be squeezed through the network before clicking the first button. Is this the best way?

## Your users aren’t waiting for your requests — why should your tests do it?

Let’s take a step back for a moment…

Why are you testing and monitoring your sites end-to-end with real browsers? You want to ensure that your site and infrastructure work properly, sure. But you also want to do this by **mimicking actual user behavior**. That’s the entire point of end-to-end testing, right?

Do your users wait for the little spinner in their URL bar to disappear before they click on something? I doubt it. I’m pretty sure your average visitor won’t look at your site’s loading state before clicking the “Add to cart” button. People click and interact with your site whenever something’s visible. That’s why the best approach to writing [Playwright scripts](https://www.checklyhq.com/blog/speed-up-playwright-scripts-request-interception/) is to stick to natural human behavior.

How should you navigate your tests, then?

The `page.goto()` waiting behavior can be tweaked with the `waitUntil` config property. And you can define four `waitUntil` settings.

```
// Wait until the HTML starts loading.
await page.goto("/", {waitUntil: "commit"});

// Wait until the HTML is parsed
// and deferred scripts (` ` and ` `) are loaded.
await page.goto("/", {waitUntil: "domcontentloaded"});

// Wait until all initially included resources are loaded.
await page.goto("/", {waitUntil: "load"});

// Wait until every resource is loaded and the network is silent for 500ms.
await page.goto("/", {waitUntil: "networkidle"});
```

When you look at these `waitUntil` options, all but `commit` heavily rely on the network and the embedded resources. This means all options but `commit` check implementation details, potentially slowing down your tests when one request is stuck in the network layer.

How do these options compare in speed? Here are the results for my example test case.

"waitUntil" speed comparison Option "page.goto" execution time

```
commit
```

62ms

```
domcontentloaded
```

159ms

```
load
```

10.1s

```
networkidle
```

12.6s

Of course, I extracted these numbers from a local Playwright test running against a local site. But the absolutes don’t matter. What’s important is to look at the differences between the `waitUntil` options.

Unsurprisingly, the `commit` waiting option is the fastest `goto()` configuration because it does not wait for anything resource-related except the first bytes of the initial HTML. `networkIdle` is by far the slowest because it waits for every resource to be loaded and then adds 500ms of idle network time on top.

But here’s a funny thing: All these tests**succeeded regardless of the `waitUntil` option.**The overall test duration for this small test case varied from roughly 10 to 25 seconds, but the tests were all green.

What is going on, and how does this work?

## Writing fast tests that don’t rely on the network

In my experience, there are only two reasons for flaky or slow tests: either your application is untestable, or you fail to follow Playwright's best practices.

If the site you want to test isn’t stable, you won’t succeed in creating a stable and fast test suite. You can’t write stable tests for a flaky app — end of story. Similarly, if your site has bad UX and includes [poor hydration patterns](https://playwright.dev/docs/navigations#hydration), your tests will be full of workarounds to succeed on the happy path. Ideally, you would fix these application issues, but I know that this is not always possible.

But if you, on the other hand, aren’t putting the user-first hat on your Playwright scripts, you’ll also be a flakiness offender. The best way to speed up and fight the flake is to rely on auto-waiting and web-first assertions and let Playwright figure out the rest.

The example script from the beginning of this post includes a “simple” `click()` instruction. And this `click()` is your best friend because it executes magic, aka [actionability checks](https://playwright.dev/docs/next/actionability).

```
// this click() will wait for the login to be
// - visible
// - stable, as in not animating or completed animation
// - able to receives events as in not obscured by other elements
// - enabled
await loginLink.click();
```

Whenever you call `click()`, `fill()` or the other actions, Playwright waits until this element is ready for the user. At this point, the element is rendered, visible, stable and enabled.

If your app now provides good UX and renders ready-to-use elements, Playwright will interact with them as quickly as possible. There’s no need to wait for any network requests.

Similarly, if you rely on [web-first assertions](https://playwright.dev/docs/best-practices#use-web-first-assertions), Playwright will wait until the UI reaches your desired state. There’s no need to wait for HTML or API calls behind a specific UI state either.

```
// wait until this element is visible
await expect(loginHeader).toBeVisible();
```

And if you’re relying on these two core Playwright principles, you can forget the network layer and test what matters — UI actions and the resulting UI state. There’s barely a need to wait for network events and requests.

login.spec.js

```
import { expect, test } from "@playwright/test";

test("Login works", async ({ page }) => {
// don’t wait for all the resources to be loaded
await page.goto("/", {waitUntil: "commit"});

// let Playwright wait for this link to be visible
const loginLink = page.getByRole("link", { name: "Login" });
await loginLink.click();

const loginHeader = page.getByRole("heading", { name: "Login" });
await expect(loginHeader).toBeVisible();

// More things to check after the login link was clicked
// ...
});
```

Looking at the adjusted script, we’re not waiting for network events but for UI state. Playwright will wait until the “Login” link is visible and click it as fast as possible. And with this minor tweak,**we made our test 10s quicker while still covering the core login functionality.** Win-win!

## But... There’s always a but…

All that said, there are scenarios when you want to keep an eye on the network.

When you adopt end-to-end testing, it’s very common to test preview deployments to avoid core feature regressions. Ideally, your preview and staging environments are production replicas. Unfortunately, that’s rarely the case. Staging is often deployed to a different infrastructure, and the frontend often loads different resources for tracking, user engagement and monitoring.

And these differences might be okay because you only want to test your new feature and avoid regressions. But when you go the extra mile and [adopt Playwright to monitor your sites](https://www.checklyhq.com/blog/[uid]/), keeping an eye on all the loaded resources will help you avoid production issues. I’ve seen a third-party script take down production more than once.

For example, we at Checkly monitor `checklyhq.com` with Playwright, and like every marketing site, we include Intercom and analytics there. One day, all our synthetic Playwright checks failed because Intercom failed to load in a reasonable time. `page.goto()` took more than a minute because of a timing-out JavaScript snippet.

Was `checklyhq.com` broken? No, everything was still functioning. Was it good to know that Intercom was having issues? You bet!

Monitoring the network layer can give you valuable insights into your site's overall health and performance.

```
// fail when all resources aren’t loaded after 10 seconds
await page.goto("/", { waitUntil: "load", timeout: 10_000 });
```

_Pro tip: [Checkly aggregates Core Web Vitals](https://www.checklyhq.com/docs/browser-checks/tracing-web-vitals/) in your Playwright scripts for you._

## Conclusion

Should you now flip every `page.goto()` action to `commit` or `domcontentloaded` to save some time in CI/CD? The answer is the usual "It depends".

If you value test execution speed, run many tests, and rely on user-first testing with auto-waiting, not waiting for all resources to be loaded will save you minutes, if not hours, in CI/CD. Give it a try, Playwright is very good at figuring out when and what to click.

But remember that a green preview deployment light isn’t giving you a proper safety net. **The only thing that matters is a well-running production environment**. To ensure that there are no production issues, you must continuously test your live product. This is when [synthetic monitoring](https://www.checklyhq.com/blog/[uid]/) shines.

And when you're then testing and monitoring your site with Playwright, keeping an eye on slow network dependencies can be very beneficial because you’ll be the first to know when something’s off with your infrastructure, the loaded third-party resources or your application code. That’s the real safety net you need to sleep well at night.

But as always, deciding what makes the most sense is up to you.

If you have any questions or comments, come and say hi in [the Checkly community](https://www.checklyhq.com/slack/). We’re a lovely bunch — I promise. And I’ll see you there!

[Stefan Judis Developer Relations](https://www.checklyhq.com/blog/author/stefan/)

In This Article

- The start of every Playwright script — page.goto()
- page.goto() and the load event
- Your users aren’t waiting for your requests — why should your tests do it?
- Writing fast tests that don’t rely on the network
- But... There’s always a but…
- Conclusion

Share on social

## Related Articles

[Improve Your Playwright Documentation with Steps March 14, 2024](https://www.checklyhq.com/blog/improve-your-playwright-documentation-with-steps/)[Leveraging Network Interception with Playwright for End-to-End Monitoring May 1, 2024](https://www.checklyhq.com/blog/monitoring-responses-in-playwright/)[Speed Up Your Playwright Scripts with Request Interception March 15, 2024](https://www.checklyhq.com/blog/speed-up-playwright-scripts-request-interception/)
