Why Does Playwright Take a Screenshot Before the Page Is Ready?
Playwright captures when your code reaches the screenshot call; wait for the specific UI state you need, not just a navigation event.
Playwright takes a screenshot when your test reaches the awaited page.screenshot() call. The call does not check whether your application has finished rendering the particular content you care about. page.goto(url) waits for the browser’s load event by default, but data fetching, hydration, and other app-specific work can continue after that event. The reliable fix is to wait for a meaningful page condition, then capture.
Wait for the state the screenshot needs
Use a web-first assertion for the content or state that must appear in the image. Playwright retries these assertions until they pass or time out.
import { test, expect } from '@playwright/test';
test('captures the ready dashboard', async ({ page }) => {
await page.goto('https://example.com/dashboard');
await expect(
page.getByRole('heading', { name: 'Dashboard' })
).toBeVisible();
await expect(page.getByTestId('report-status')).toHaveText('Ready');
await page.screenshot({ path: 'dashboard.png', fullPage: true });
});
Replace the example URL, heading, and test ID with conditions that represent your app’s actual ready state. If only one panel needs to be ready, assert that panel instead of waiting for unrelated page content.
What Playwright’s navigation waits guarantee
The waitUntil option controls which navigation milestone page.goto() waits for. These milestones describe document or network progress; they do not establish that your application-specific state is ready.
| Condition | What it means | When to use it |
|---|---|---|
commit |
The response is received and document loading has started. | When you intentionally need to proceed very early in navigation. |
domcontentloaded |
The target frame fires DOMContentLoaded. |
When DOM parsing is sufficient for the next operation. |
load |
The target frame fires load. This is the default for page.goto(). |
When the browser load event is the needed milestone. |
networkidle |
There are no network connections for at least 500 ms. | Playwright discourages using this as a general testing readiness condition; prefer a web assertion. |
For example, this explicitly proceeds at DOM parsing rather than the default load event:
await page.goto('https://example.com/dashboard', {
waitUntil: 'domcontentloaded',
});
await expect(page.getByTestId('report-status')).toHaveText('Ready');
await page.screenshot({ path: 'dashboard.png' });
Changing waitUntil may make navigation return earlier or later, but an assertion on the required UI state is still what ties the screenshot to the content you need. See the Page API navigation and screenshot documentation and the Playwright writing tests guide.
Choose a readiness condition that matches the content
- Text or data: assert the expected text, status, or value, such as
toHaveText('Ready'). - A component: assert that the specific locator is visible.
- An image: wait for the image to be visible and, if completeness matters, verify its loaded state. A visible image element alone may not mean its image data has loaded.
- A visual regression: use
toHaveScreenshot()when you want Playwright Test to compare a screenshot with its expected image, while still waiting for the app’s intended state first.
await expect(page.getByTestId('chart')).toBeVisible();
await expect(page.getByTestId('chart-status')).toHaveText('Loaded');
await expect(page).toHaveScreenshot('dashboard.png');
Locator actions also wait for actionability conditions on their target. That is different from waiting for every other part of the page to finish rendering.
Why screenshots can still look incomplete
- The app renders after the load event. Client-side data, hydration, delayed widgets, or user-triggered content may arrive later. Confirm which visible app state is missing, then assert it.
- The navigation wait is early. Check whether
waitUntilis set tocommitordomcontentloaded. Those conditions return beforeload. - A previous action navigated. Inspect the operation that triggered navigation and the condition it waits for before the screenshot step.
- A locator action succeeded. Its target became actionable; that does not guarantee that a separate data panel or image is ready.
- The test uses a fixed delay or network quietness. A timeout may be too short on a slow run and unnecessarily long on a fast one. Background connections can also prevent network quietness. Assert the app state that matters.
Without the test code, URL, app behavior, and screenshot sequence, one specific capture cannot be diagnosed. The general cause is that the awaited condition and the state considered ready do not match.
Make screenshot tests reliable and efficient
- Identify the visible symptom. Name the exact missing text, component, image, or state.
- Wait on that condition. Use a locator assertion that describes what should be present.
- Keep the capture close to the assertion. Avoid unrelated actions that might navigate or change the page between readiness and capture.
- Set timeouts deliberately. Web-first assertions retry until they pass or their assertion timeout is reached. Choose a timeout appropriate for the app and test environment rather than masking synchronization with a long sleep.
- Use screenshot comparison for comparison.
toHaveScreenshot()can check visual output, but it does not replace waiting for the application state your test intends to compare.
Waiting on a specific condition avoids time spent sleeping after the page is already ready and avoids capturing before it is ready. It also makes failures more informative: an assertion timeout points to the expected state that did not appear.
Or skip the browser setup
If you need a website image outside a Playwright test, ScreenshotNeo provides a screenshot API and MCP server. A single GET request returns an image or PDF. See the API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card.
Troubleshooting
| Symptom | Likely cause | What to change |
|---|---|---|
| Screenshot is taken before content appears | The screenshot follows a navigation milestone, but app content renders afterward. | Assert on the expected text or component before capturing. |
goto() returns sooner than expected |
waitUntil is set to commit or domcontentloaded. |
Use the appropriate lifecycle condition, then assert the required app state. |
| Assertion times out | The locator or expected value does not match, the state never appears, or the assertion timeout is too short for the environment. | Check the locator and expected state, inspect why the app did not reach it, then adjust the timeout if the state is legitimately slower. |
networkidle never arrives or gives inconsistent results |
Persistent or background network activity prevents a stable quiet period, or network quietness does not correspond to UI readiness. | Use a web-first assertion for the state needed in the screenshot. |
| One region is ready but another is not | The test waits for only one component, while the screenshot depends on another. | Add an assertion for each essential region, or define one app-level ready indicator that means all required content is ready. |
| Screenshot assertion differs between runs | The page may still be changing when comparison starts. | Wait for the meaningful state first, then compare with toHaveScreenshot(). |
FAQ
Does page.screenshot() wait for the page to finish?
It captures when the test reaches the call. It does not determine whether your app’s content is ready.
Is load the same as application readiness?
No. It is a browser lifecycle event. Application-specific work can continue after it.
Should I always wait for networkidle?
No. Playwright discourages it as a general test readiness condition. Prefer an assertion on the page state you need.
Why didn’t clicking a button make the whole page ready?
Actionability waits concern the locator used for that action. They do not promise that unrelated content has finished loading.


