I'm trying to figure out how to check whether the page has fully loaded in Playwright. await page.waitForLoadState('networkidle');
doesn't always work for me on Javascript-heavy sites. I've resorted to taking a screenshot base64
, waiting 100 ms, taking a new screenshot, and comparing whether those are the same. However this doesn't seem ideal, is there any way to ask Playwright when the last animation frame was redrawn?

- 634
- 1
- 6
- 20
-
See the answer suggested by playwright team - https://stackoverflow.com/a/76623102 – Vishal Aggarwal Sep 01 '23 at 09:32
6 Answers
There are several options that may help you.
1. Solution 1:
First, you can maybe determine which element is loading last, and then go with
page.waitForSelector('yourselector')
or even wait for multiple selectors to appear
page.waitForSelector('yourselector1','yourselector2')
2. Solution 2
page.waitForLoadState('domcontentloaded')
Because
page.waitForLoadState('networkidle')
by default will wait for network event and if 0.5 seconds nothing is network trafficking it will say, I am no longer need to wait. And that is why you maybe have stohastic beh.

- 349
- 1
- 3
- 14

- 1,538
- 15
- 32
-
Is there any way to hook to screencastframe event from here: https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/server/chromium/videoRecorder.ts#L56 I'm also recording a video, if I could listen for this event, I could just wait until enough time passed – eeeeeeeeeeeeeeeeeeeeeeeeeeeeee Apr 20 '22 at 11:56
-
And just doing this doesn't work + shows TS warning: `page.on('screencastframe', () => { console.log(1231323);})` – eeeeeeeeeeeeeeeeeeeeeeeeeeeeee Apr 20 '22 at 11:59
-
Not sure what you are recording, but in playwright you have option to do video record, rather then implement your own. In config file, just sett video "On", and in reporter look for the trace file, there you can see video execution and how page is loading for you. If you dont find option , post comment, I will find it for you. – Gaj Julije Apr 20 '22 at 14:25
-
When using `page.goto()`, you can pass it directly as 2nd arg: `await page.goto(`/my-page`, { waitUntil: 'networkidle' })`. – Janosh Sep 03 '22 at 04:09
doesn't always work for me on Javascript-heavy sites
In my case the page was a SPA, and was changing even after it was reported as loaded.
I used a basic debouncer: it waits for a specific portion of the DOM (that I'm interested in) to stop changing for 1000 milliseconds.
public async Task DebounceDom(ILocator locator, int pollDelay = 100, int stableDelay = 1000)
{
var markupPrevious = "";
var timerStart = DateTime.UtcNow;
var isStable = false;
while (!isStable)
{
var markupCurrent = await locator.InnerHTMLAsync();
if (markupCurrent == markupPrevious)
{
var elapsed = (DateTime.UtcNow - timerStart).TotalMilliseconds;
isStable = stableDelay <= elapsed;
}
else
{
markupPrevious = markupCurrent;
}
if (!isStable) await Task.Delay(pollDelay);
}
}
I'm using the .NET Playwright bindings, but you could easily translate that to JS or Python.
It's potentially expensive, so don't use it unless you must, and choose the delays wisely.

- 14,255
- 23
- 85
- 176
-
1it seems like await Task.Delay(pollDelay) - should be not in the context of the else block, but in the context of while block. – Serhiy Tymoshenko Apr 30 '23 at 14:27
-
1@SerhiyTymoshenko Nice catch! I also added a check to ensure it doesn't wait unnecessarily at the end. Also nice CS->TS conversion. – lonix Apr 30 '23 at 16:40
Typescript version based on @lonix answer. Also I've used a general approach polling whole document.body instead of particular DOM node, as in my case it wasn't working - markupCurrent was stable starting with page load while the animations continued to perform.
async debounceDom(pollDelay = 50, stableDelay = 350) {
let markupPrevious = '';
const timerStart = new Date();
let isStable = false;
while (!isStable) {
const markupCurrent = await this.page.evaluate(() => document.body.innerHTML);
if (markupCurrent == markupPrevious) {
const elapsed = new Date().getTime() - timerStart.getTime();
isStable = stableDelay <= elapsed;
} else {
markupPrevious = markupCurrent;
}
if (!isStable) await new Promise(resolve => setTimeout(resolve, pollDelay));
}
}
Self-explaining method for those want to check what is going on with the page
async debounceDomLog(pollDelay = 50, stableDelay = 350) {
let markupPrevious = '';
const timerStart = new Date();
let isStable = false;
let counter = 0;
while (!isStable) {
++counter;
console.log('-----\nattempt: ', counter);
const markupCurrent = await this.page.evaluate(() => document.body.innerHTML);
const elapsed = new Date().getTime() - timerStart.getTime();
if (markupCurrent == markupPrevious) {
isStable = stableDelay <= elapsed;
if (!isStable) {
console.log('size is stable! Still polling... (Reason: delay not elapsed yet)');
console.log('this attempt size: ', markupCurrent.length);
} else {
console.log('size settled and time elapsed');
console.log('this attempt size: ', markupCurrent.length);
}
} else {
console.log('this attempt size: ', markupPrevious.length);
markupPrevious = markupCurrent;
}
if (!isStable) {
console.log('waiting for poll delay: ', pollDelay);
await new Promise(resolve => setTimeout(resolve, pollDelay));
}
console.log('elapsed: ', elapsed)
}
}
and the resulting output:
-----
attempt: 1
this attempt size: 0
waiting for poll delay: 25
elapsed: 113
-----
attempt: 2
this attempt size: 1612125
waiting for poll delay: 25
elapsed: 208
-----
attempt: 3
size is stable! Still polling... (Reason: delay not elapsed yet)
this attempt size: 1608951
waiting for poll delay: 25
elapsed: 304
-----
attempt: 4
size settled and time elapsed
this attempt size: 1608951
elapsed: 400
1 passed (11.5s)

- 332
- 2
- 6
-
1Probably a good idea to include a check at the end, like I did in my edit. – lonix May 01 '23 at 08:05
If the given solution doesn't work for you, you can try with locator.
page.locator(selector[, options])
It has multiple api like locator.isDisabled or locator.waitFor([options]) or locator.isVisible([options]) or locator.frameLocator(selector) ....... a lot more.
see the below link here: https://playwright.dev/docs/api/class-locator

- 349
- 1
- 3
- 14

- 2,954
- 2
- 16
- 29
-
1Yeah this still needs me to know that to look for on the next page. I'd like to just have a general-purpose method `gotoAndWaitTIllIts100PercentLoaded` – eeeeeeeeeeeeeeeeeeeeeeeeeeeeee Apr 20 '22 at 13:58
-
or just use await new Promise(r => setTimeout(r, 100)); https://stackoverflow.com/questions/951021/what-is-the-javascript-version-of-sleep – Anonymous Type Oct 03 '22 at 02:48
How to wait till all animations has finished loading?
You can wait till the animations on last(visually) loaded element has finished:
import type { Page } from '@playwright/test'
function waitForAnimationEnd(page: Page, selector: string) {
return page
.locator(selector)
.evaluate((element) =>
Promise.all(
element
.getAnimations()
.map((animation) => animation.finished)
)
)
}
test(`Wait till last element animations has finished loading `, async ({ page }) => {
await wait_for_animation_end(page, `lastLoadedElement`)
})
Reference:https://github.com/microsoft/playwright/issues/15660

- 1,929
- 1
- 13
- 23
If you really need to wait for the page to stop visually changing, you can write a simple function that will take the page screenshot, compare it with the previous one and quit if they are equal.
I use this function:
def wait_animation_stops(page, timeout=30):
t = time()
old_image = page.screenshot(full_page=True)
while time() - t < timeout:
sleep(0.5)
new_image = page.screenshot(full_page=True)
if old_image == new_image:
print(f'Animation stopped after {time() - t} seconds')
return True
old_image = new_image
print(f'Animation did not stop in {timeout} seconds')
return False

- 49,934
- 160
- 51
- 83

- 23
- 5