To make Playwright wait for a page to load, start with page.goto(). Then wait for the content that matters to your task.
For screenshots, that might be a report becoming visible, a chart finishing its render, or the images inside a product card loading. Those events can happen after the browser considers the document loaded.
This guide focuses on those waiting decisions. For viewport settings, element capture, and other screenshot options, see the broader guide on how to render screenshots with Playwright.
Quick example: wait before taking a screenshot
Install Playwright and its Chromium browser:
npm install playwrightnpx playwright install chromiumSave this as screenshot.mjs and run it with node screenshot.mjs:
import { chromium } from "playwright";
const browser = await chromium.launch();
try { const page = await browser.newPage({ viewport: { width: 1280, height: 800 }, });
await page.goto("https://example.com", { waitUntil: "domcontentloaded", timeout: 30_000, });
await page.getByRole("heading", { name: "Example Domain" }).waitFor({ state: "visible", timeout: 10_000, });
await page.evaluate(async () => { await document.fonts.ready; });
await page.screenshot({ path: "screenshot.png", fullPage: true, animations: "disabled", caret: "hide", });} finally { await browser.close();}The heading is a useful readiness signal for this simple page. On your own application, replace it with a signal that appears when the desired content is ready, such as a completed report or a populated results table.
animations: "disabled" handles CSS animations, CSS transitions, and Web Animations. It does not freeze every JavaScript animation, canvas, or video on a page. See Playwright’s screenshot options.
Which wait should you use?
| What you need | Playwright method | Example |
|---|---|---|
| Open a page | page.goto() | Navigate to a URL and wait for a document event. |
| Wait for a document event on the current page | page.waitForLoadState() | Wait for load after navigation has started. |
| Wait for content to appear | locator.waitFor() | Wait for a report’s ready state to become visible. |
| Follow navigation after a click | page.waitForURL() | Wait for the dashboard URL while submitting a form. |
| Wait for a particular request to finish | page.waitForResponse() | Wait for the report API response. |
| Wait for a condition in the page | page.waitForFunction() | Wait until a chart exposes its completed state. |
Choose the wait based on the event you expect. A browser event, a network response, and a visible report each answer a different question.
page.goto and waitUntil
Without options, page.goto(url) waits for the load event:
await page.goto("https://example.com");You can choose a different navigation milestone with waitUntil:
| Value | What it means | Screenshot consideration |
|---|---|---|
commit | The response arrived and the document started loading. | Too early for most screenshots. |
domcontentloaded | The document was parsed. | Follow it with a content-specific wait. |
load | The document’s load event fired. | A useful default, but later application updates can still happen. |
networkidle | No network connections for at least 500 ms. | A heuristic that can delay capture without proving the desired content is ready. |
These are document lifecycle milestones. They do not describe every asynchronous update inside an application.
For example, a page can load its HTML and then request the data for a chart. The screenshot should wait for the chart’s completed state, even if load has already fired.
Wait for a selector or locator
For dynamic content, a locator is often the clearest option:
await page.goto("https://example.com/report", { waitUntil: "domcontentloaded",});
await page.locator('.report[data-ready="true"]').waitFor({ state: "visible", timeout: 30_000,});
await page.screenshot({ path: "report.png", fullPage: true });Here, data-ready="true" is an example of a signal your application provides after it has rendered the report. Replace the URL and selector with those from your site.
Waiting for an empty report container is weaker: the container might exist before any data arrives. A spinner disappearing, a result count appearing, or an application-provided ready attribute can be more useful.
For new code, Playwright recommends locator-based waits. page.waitForSelector() still exists, but locators fit better with the rest of Playwright’s API.
What Playwright auto-waiting covers
Before actions such as locator.click(), Playwright checks whether the target is actionable. That can include whether it is visible, stable, enabled, and able to receive events. See the auto-waiting documentation.
That helps you interact with a page. It does not tell Playwright which business state you want to capture afterward.
For example, a button can be ready to click while the report it generates still takes another second to render:
await page.getByRole("button", { name: "Generate report" }).click();
await page.locator('.report[data-ready="true"]').waitFor({ state: "visible",});For tests, Playwright’s retrying assertions are another useful choice. For a standalone screenshot script, a locator wait gives you a straightforward readiness check without adding a test runner.
Wait for navigation after a click
Start waiting for the destination as part of the action that triggers navigation:
await Promise.all([ page.waitForURL("**/dashboard", { waitUntil: "domcontentloaded", timeout: 30_000, }), page.getByRole("link", { name: "Dashboard" }).click(),]);
await page.locator('[data-dashboard-ready="true"]').waitFor({ state: "visible",});The URL establishes that you reached the destination. The locator establishes that its content is ready.
Playwright deprecates page.waitForNavigation() and recommends page.waitForURL(). Its navigation guide also explains why page load and application readiness can differ.
If your code is failing during redirects or page.evaluate(), see how to fix the Playwright execution context error.
Wait for an API response and then the rendered result
If a button fetches data without changing the URL, register a response wait before clicking:
const reportResponse = page.waitForResponse( (response) => response.url().endsWith("/api/report") && response.request().method() === "GET" && response.ok(), { timeout: 30_000 });
await page.getByRole("button", { name: "Load report" }).click();await reportResponse;
await page.locator('.report[data-ready="true"]').waitFor({ state: "visible",});Adjust the URL predicate to match your application. For repeated loads, make the readiness signal identify the new result, rather than an earlier report already on the page.
A successful response means the data arrived. Rendering may still happen afterward, which is why the example also waits for the completed report.
Wait for fonts and images
Once the desired content exists, wait for fonts before capturing text:
await page.evaluate(async () => { await document.fonts.ready;});For images inside a known section, you can add a bounded check:
await page.locator(".report").waitFor({ state: "visible" });
await page.waitForFunction( () => Array.from(document.querySelectorAll(".report img")).every( (image) => image.complete && image.naturalWidth > 0 ), null, { timeout: 10_000 });This checks image elements in the report, not CSS background images. A broken image will cause a timeout, which is useful when a missing image should fail the capture.
Lazy-loaded images below the viewport may not have been requested yet. Scroll the relevant content into view before checking it. Setting fullPage: true alone does not perform the scrolling needed to trigger every lazy-loading implementation.
For long pages, see the full-page screenshot guide.
When waitForLoadState is useful
Use page.waitForLoadState("load") when the current page has begun navigation and you need to wait for that document event.
After this code, a second load wait is usually redundant:
await page.goto("https://example.com");await page.waitForLoadState("load");goto() already waited for load. Calling waitForLoadState() again does not restart loading or wait for a later React update.
Why networkidle and fixed delays can disappoint
A page can keep making background requests long after its useful content appears. Conversely, the network can become quiet before a timer or client-side computation updates the page.
That makes networkidle a poor universal definition of screenshot readiness. Playwright explicitly discourages it as a readiness check for tests in its load-state documentation.
A fixed delay has a similar trade-off:
await page.waitForTimeout(5000);Five seconds can be too short on a slow page and unnecessarily long on a fast one. Use it while diagnosing a timing issue, or as a measured fallback when a third-party page has no useful readiness signal. Prefer an observable condition when one exists.
The same approach in Python
Install the Python package and browser:
python -m pip install playwrightpython -m playwright install chromiumThen use wait_until for navigation and a locator for content readiness:
from playwright.sync_api import sync_playwright
with sync_playwright() as playwright: browser = playwright.chromium.launch() try: page = browser.new_page(viewport={"width": 1280, "height": 800}) page.goto("https://example.com", wait_until="domcontentloaded") page.get_by_role("heading", name="Example Domain").wait_for( state="visible", timeout=10_000 ) page.evaluate("async () => { await document.fonts.ready; }") page.screenshot(path="screenshot.png", full_page=True) finally: browser.close()The Python Playwright screenshot guide covers more output and browser options.
Use ScreenshotOne for managed screenshot rendering
If your application needs website screenshots, ScreenshotOne gives you rendering controls through an HTTP API. You can set a load event, wait for a selector, block cookie banners, and capture a full page without running Playwright in your application.
curl --fail-with-body --get "https://api.screenshotone.com/take" \ --data-urlencode "access_key=YOUR_ACCESS_KEY" \ --data-urlencode "url=https://example.com" \ --data-urlencode "wait_until=load" \ --data-urlencode "full_page=true" \ --data-urlencode "block_cookie_banners=true" \ --data-urlencode "block_ads=true" \ --output screenshot.pngFor a dynamic page, add --data-urlencode 'wait_for_selector=.report[data-ready="true"]' with your own selector. ScreenshotOne’s selector wait checks for DOM presence, so choose a selector whose presence represents the finished content. It is not equivalent to Playwright’s visible locator wait.
See the ScreenshotOne wait options and full-page rendering options. You can also compare the approaches in ScreenshotOne vs ScrapingBee for screenshots.
Frequently Asked Questions
If you read the article, but still have questions. Please, check the most frequently asked. And if you still have questions, feel free reach out at support@screenshotone.com.
How do I wait for a page to load in Playwright?
Use page.goto(url) for initial navigation. It waits for the load event by default. For a JavaScript application, also wait for the element or application state you need before reading content or taking a screenshot.
Should I use networkidle before every Playwright screenshot?
No. Network silence does not guarantee that the content you need has rendered. Prefer a visible locator or a specific readiness condition. Playwright also discourages using networkidle as a general readiness check for tests.
Does waitForLoadState wait for React or Vue to finish rendering?
No. It waits for a document lifecycle event. A React or Vue application can update after that event, so wait for a ready element, response, or application-specific condition as well.


