Waits & Sync October 28, 2026 8 min read

Playwright waitForLoadState: When You Need It and When You Don't

page.waitForLoadState() waits for load, domcontentloaded or networkidle. It's often a no-op after goto, but it's exactly what you need for popups and new tabs. Learn the default state, when it resolves instantly, the popup pattern, why networkidle is discouraged, and how it compares with goto's waitUntil and waitForURL. Runnable on Playwright 1.62.

Playwright waitForLoadState pauses a test until the page reaches a load state: load, domcontentloaded or networkidle. Most of the time you don't need it, because page.goto() already waits for load. Where it earns its place is on pages you didn't open with goto, such as popups and new tabs. For choosing between all the waits, start with the Playwright wait guide.

Below: the three states, the default, when the call resolves instantly, the popup pattern, why networkidle is discouraged, and how it compares with goto's waitUntil and with waitForURL. Every example is JavaScript and was run on Playwright 1.62.

Quick answer: await popup.waitForLoadState() waits for the load event on a page you got from waitForEvent('popup'). If the state was already reached, it returns immediately. Don't use 'networkidle' in tests; assert on content instead.


page.waitForLoadState() Syntax and States

Playwright waitForLoadState is a page method that waits until the current document reaches a load state: load (the default), domcontentloaded or networkidle. If the page has already reached that state, it returns immediately, so it is mainly useful for popups and new tabs.

Signature
await page.waitForLoadState(state, options);

// state:   'load' (default) | 'domcontentloaded' | 'networkidle'
// options: { timeout?: number }
StateResolves when…Verdict
'load' (default)the load event fires: HTML, scripts, styles and images are doneSafe default
'domcontentloaded'the DOMContentLoaded event fires: the HTML is parsedFaster, enough for most checks
'networkidle'there have been no network connections for at least 500 msDiscouraged by Playwright

Note there is no 'commit' state here. commit exists only as a waitUntil value for goto, waitForURL and similar navigation methods.

Timeout: in Playwright Test the default comes from navigationTimeout, which is 0 (no limit) unless you set it, so the 30-second test timeout is what stops a stuck wait. Pass { timeout: 10000 } to fail sooner.


When waitForLoadState Resolves Immediately

If the current document has already reached the state, waitForLoadState returns at once. That's the most common reason it seems to "do nothing": after await page.goto(url), the load event has already fired, so a following await page.waitForLoadState() is a no-op.

tests/waitforloadstate.spec.js
const { test, expect } = require('@playwright/test');

test('resolves at once if the state is already reached', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/login'); // already waited for 'load'
  await page.waitForLoadState(); // returns immediately: 'load' already fired
  await expect(page.getByRole('button', { name: 'Login' })).toBeVisible();
});

test('goto fast, then wait for the full load', async ({ page }) => {
  await page.goto('https://playwright.dev/', { waitUntil: 'domcontentloaded' });
  // ...quick checks that only need the HTML...
  await expect(page).toHaveTitle(/Playwright/);

  await page.waitForLoadState('load'); // now wait for images, styles, scripts
});

test('wait for a popup to load', async ({ page }) => {
  await page.goto('https://the-internet.herokuapp.com/windows');

  const popupPromise = page.waitForEvent('popup');
  await page.getByRole('link', { name: 'Click Here' }).click();
  const popup = await popupPromise;
  await popup.waitForLoadState();

  await expect(popup).toHaveTitle('New Window');
  await expect(popup.getByRole('heading')).toHaveText('New Window');
});

test('wait for a new tab via the context', async ({ page, context }) => {
  await page.goto('https://the-internet.herokuapp.com/windows');

  const pagePromise = context.waitForEvent('page');
  await page.getByRole('link', { name: 'Click Here' }).click();
  const newPage = await pagePromise;
  await newPage.waitForLoadState('domcontentloaded');

  await expect(newPage).toHaveURL(/\/windows\/new$/);
});

The first two tests show the useful and useless cases on the same page object. The second one is a real pattern: go to the page with waitUntil: 'domcontentloaded', run quick checks that only need the HTML, then call waitForLoadState('load') when you need everything else.

One more rule from the Playwright docs: the navigation must already be committed when you call it. waitForLoadState checks the document the page has now; it doesn't wait for a navigation that hasn't started. After a click that navigates, use page.waitForURL() instead, explained in the waitForURL guide.

Waiting for "the page to load" and still failing? Load states measure the browser, not your app. When a test waits on the wrong signal, it fails at random. Your tests aren't flaky, your waits are: see the fix →


This is where waitForLoadState is genuinely needed. A popup is a new page that Playwright hands you as soon as it exists, possibly before it has finished loading. Nobody called goto on it, so nobody waited for load:

Snippet
const popupPromise = page.waitForEvent('popup');      // 1. start listening
await page.getByRole('link', { name: 'Click Here' }).click(); // 2. trigger
const popup = await popupPromise;                     // 3. get the new page
await popup.waitForLoadState();                       // 4. wait for 'load'

The third and fourth tests in the file above run this on the-internet's /windows page, which opens /windows/new in a new tab. Use page.waitForEvent('popup') when the tab was opened by that page; use context.waitForEvent('page') when you want any new page in the browser context. Either way, start the listener before the click, or the event can fire before you're listening.


Why networkidle Is Discouraged

The Playwright API docs label 'networkidle' DISCOURAGED: "Don't use this method for testing, rely on web assertions to assess readiness instead." Three reasons:

  • It can hang. Analytics beacons, long polling, chat widgets and websockets keep the network busy, so the 500 ms quiet window may never come.
  • It can pass too early. If the app pauses before firing its data request, the page looks idle and the wait resolves before the data arrives.
  • It measures the wrong thing. Your test cares that the order list is on screen, not that the network went quiet.
Snippet
// Avoid
await page.goto('https://demo.playwright.dev/api-mocking');
await page.waitForLoadState('networkidle');

// Prefer: wait for the content the API call produces
await page.goto('https://demo.playwright.dev/api-mocking');
await expect(page.getByText('Strawberry')).toBeVisible();

If you need a specific request to finish, wait for that request with page.waitForResponse(). It's precise where networkidle is a guess.


waitForLoadState vs goto's waitUntil

page.goto(url, { waitUntil }) navigates and waits in one call; waitForLoadState(state) only waits, on whatever document is already there. These two lines end in the same place:

Snippet
await page.goto('https://playwright.dev/', { waitUntil: 'domcontentloaded' });

// is the same wait as
await page.goto('https://playwright.dev/', { waitUntil: 'commit' });
await page.waitForLoadState('domcontentloaded');

So when you control the navigation, set waitUntil on goto and skip waitForLoadState. Reach for waitForLoadState when you didn't start the navigation: popups, new tabs, or upgrading from a fast domcontentloaded to a full load later in the test.


waitForLoadState vs waitForURL

waitForLoadState()waitForURL()
Waits forA load state of the current documentA matching URL, then a load state
After a click that navigatesMay check the old page and return at onceWaits for the new URL
SPA route changesNo new document, nothing to wait forWorks (History API counts)
New popup or tabThe right toolWorks if you know the URL

Rule of thumb: same tab, new URL → waitForURL. New tab → waitForEvent('popup') + waitForLoadState. Content → an assertion. If a wait still times out, the timeout errors guide and how to debug Playwright tests will find the cause, and the flaky tests fix guide covers the rest.

"Wait for the page to load" is where flaky tests begin.

See the waits-first approach that makes Playwright tests stable, in JavaScript, with Claude AI helping you write and fix them.

See how to fix it →

Frequently Asked Questions

What is the default state of waitForLoadState in Playwright?

The default is 'load', which waits for the load event. You can pass 'domcontentloaded' to wait only until the HTML is parsed, or 'networkidle', which Playwright discourages for testing.

Why does waitForLoadState return immediately?

If the current document has already reached the requested state, it resolves at once. After await page.goto(url), the load event has already fired, so a following page.waitForLoadState() has nothing to wait for.

How do I wait for a popup to load in Playwright?

Start listening before the click: const popupPromise = page.waitForEvent('popup'); then click, then const popup = await popupPromise; and await popup.waitForLoadState(). Then assert on the popup's content.

Is networkidle deprecated in Playwright?

It isn't removed, but Playwright's docs mark it as discouraged and say not to use it for testing. It waits for 500 ms without network connections, which some pages never reach. Assert on content or use page.waitForResponse() instead.

What is the difference between waitForLoadState and goto's waitUntil?

goto(url, { waitUntil }) navigates and waits in one call. waitForLoadState(state) only waits, on the document that is already loading. Use waitUntil when you control the navigation, and waitForLoadState for popups, new tabs or a later upgrade to a full load.

Should I use waitForLoadState after a click?

Usually not. It checks the current document and may return before the navigation starts. Use page.waitForURL() for a same-tab navigation, or an assertion on the new content.


Asim Noaman - Playwright and Claude AI course instructor

Asim Noaman

Senior QA Automation Engineer & AI Testing Specialist

With years of hands-on experience building test automation frameworks for production applications, Asim specializes in combining traditional QA methodologies with cutting-edge AI tools. He has helped teams adopt Playwright and AI-driven testing workflows to ship faster with fewer bugs.

Udemy Instructor Published course author
Playwright + AI Expert Specialized in AI-powered QA
Production Experience Enterprise-grade frameworks
Connect on LinkedIn

Playwright + Claude AI Course

"Wait for the Page to Load" Isn't a Test Strategy.

Load states describe the browser, not your app. Tests that rely on them pass on your laptop and fail in CI. The course shows you how to wait for what users actually see, in JavaScript, and how Claude AI and the Playwright MCP Server help you find the waits that break.

  • Popups, new tabs and multi-page flows that stay stable
  • Content-based waits instead of networkidle
  • Claude AI + Playwright MCP Server for writing and fixing tests
  • 4.5★ on Udemy, 510+ students, 30-day money-back guarantee
Stop the Flaky Tests →

Ready now? Enroll on Udemy · Know your waits? Take the practice tests