How to Wait for a Website to Finish Rendering Before Taking a Screenshot
Wait for a page milestone, then confirm the specific content your screenshot needs is ready. See Playwright and Selenium examples, common pitfalls, and a no-browser-setup option.
There is no browser event that reliably means every website has finished rendering. Navigate to an appropriate document milestone, then wait for the specific content or state that must appear in the screenshot. In Playwright, that usually means waiting for a locator or web-first assertion. A fixed delay or networkidle alone cannot prove that an application is visually ready.
This guide shows how to wait for application readiness in Playwright and Selenium, choose a useful signal, handle common loading patterns, and make screenshot output more repeatable.
1. Choose what “ready” means for your screenshot
A page can reach a browser lifecycle milestone while its JavaScript application is still fetching data, rendering components, loading images, or changing layout. Define readiness in terms of the screenshot’s purpose: for example, a chart is visible, a heading has the expected text, a loading indicator is gone, or a results list contains the expected item.
| Signal | What it indicates | Good fit | Limitation |
|---|---|---|---|
commit |
A response arrived and document loading began. | Starting work as early as possible. | Almost nothing about rendered content. |
domcontentloaded |
The browser fired DOMContentLoaded. |
Pages whose needed DOM is available early. | Does not wait for all assets or app updates. |
load |
The browser fired the document load event. | Pages that need load-event resources. | JavaScript may continue changing the page afterward. |
networkidle |
Playwright observed no network connections for at least 500 ms. | Occasional sites where network quiet is a useful additional clue. | Not proof that the target looks right; ongoing polling can prevent it. Playwright discourages it for tests. |
| Target locator or assertion | A specific element or expected state is present. | Most app screenshots. | Choose a signal that reflects the actual visual requirement. |
| Stable screenshot pixels | Repeated captures match under a screenshot assertion’s rules. | Visual regression tests. | Pixel stability does not establish semantic correctness. |
Playwright documents these navigation milestones and recommends web assertions to assess readiness instead of using networkidle for tests. [Playwright Page API] Selenium makes a similar distinction: navigation waits for a readyState according to the page-load strategy, but JavaScript may still change the page afterward. [Selenium waiting strategies]
2. Playwright: wait for the content you plan to capture
Install Playwright and its Chromium browser in a Node.js project:
npm install playwright
npx playwright install chromium
Save this as screenshot.mjs, then run node screenshot.mjs https://example.com. Replace the example selector and expected text with a stable signal from the page being captured.
import { chromium } from 'playwright';
const url = process.argv[2] ?? 'https://example.com';
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });
// Wait for the content this screenshot needs, not an arbitrary sleep.
const report = page.locator('[data-testid="report"]');
await report.waitFor({ state: 'visible', timeout: 20_000 });
await page.getByText('Quarterly results', { exact: true }).waitFor({ state: 'visible' });
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
Use a web-first assertion when you have Playwright Test
In a Playwright Test test, assertions retry until they pass or time out. This makes the expected state explicit and avoids timing guesses.
import { test, expect } from '@playwright/test';
test('capture the rendered report', async ({ page }) => {
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await expect(page.getByTestId('report')).toBeVisible();
await expect(page.getByRole('heading', { name: 'Quarterly results' })).toBeVisible();
await page.screenshot({ path: 'report.png', fullPage: true });
});
For visual regression checks, use Playwright Test’s screenshot assertion when its behavior matches your goal:
await expect(page).toHaveScreenshot('report.png', {
fullPage: true,
animations: 'disabled'
});
toHaveScreenshot waits until two consecutive screenshots yield the same result before comparing with the expectation. This is useful for visual comparisons, but it is not a general detector that application data is correct or complete. Disabling animations stops CSS animations, transitions, and Web Animations during capture; finite animations are fast-forwarded and infinite animations are canceled and later resumed. [Playwright PageAssertions]
3. Selenium: wait for a specific condition after navigation
Selenium navigation commands use a page-load strategy and normally wait for readyState to reach complete. That does not guarantee that client-side rendering is finished. Add an explicit wait for the target element or condition.
Install Selenium with Python and ensure a compatible browser and driver are available in your environment:
python -m pip install selenium
Save as screenshot.py and run python screenshot.py https://example.com:
import sys
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
url = sys.argv[1] if len(sys.argv) > 1 else 'https://example.com'
driver = webdriver.Chrome()
try:
driver.set_window_size(1440, 1000)
driver.get(url)
wait = WebDriverWait(driver, 20)
report = wait.until(EC.visibility_of_element_located(
(By.CSS_SELECTOR, '[data-testid="report"]')
))
wait.until(EC.text_to_be_present_in_element(
(By.CSS_SELECTOR, 'h1'), 'Quarterly results'
))
driver.save_screenshot('page.png')
finally:
driver.quit()
For a full-page image, Selenium’s basic screenshot command may capture only the current viewport depending on browser and driver. If the whole document is required, use a browser-specific full-page capture method or scroll and stitch viewport captures, accounting for sticky headers and lazy-loaded content.
4. Wait for the right signal in common page patterns
Client-side data or a loading spinner
Wait for the result container to become visible and, if possible, for the spinner to disappear or a known result to appear. Visibility alone may occur while the container is still empty.
await page.locator('[data-testid="loading"]').waitFor({ state: 'hidden' });
await page.locator('[data-testid="results"] li').first().waitFor({ state: 'visible' });
Lazy-loaded images
Full-page capture may trigger lazy loading differently across sites and browsers. Scroll through the document before capture, then wait for relevant images to finish loading. A useful page-side condition is that each image is complete and has a nonzero natural width.
await page.evaluate(async () => {
const step = Math.max(300, Math.floor(window.innerHeight * 0.8));
for (let y = 0; y < document.body.scrollHeight; y += step) {
window.scrollTo(0, y);
await new Promise(resolve => setTimeout(resolve, 100));
}
window.scrollTo(0, 0);
});
await page.waitForFunction(() =>
[...document.images].every(img => img.complete && img.naturalWidth > 0)
);
Some pages add images after scrolling or intentionally keep broken images. In those cases, wait only for the image selectors that matter and use a bounded timeout.
Charts, maps, and canvas content
A canvas can exist before its drawing is complete. Prefer an app-owned readiness marker, a known legend or label, or an explicit signal exposed by the chart component. If none exists, a short bounded settle interval after the data-ready state can help, but it is heuristic and should be kept separate from the main readiness condition.
Animations and moving content
For visual tests, disable animations during capture if the expected image is meant to be static. For product documentation or user-facing captures, disabling animation may produce a state users never see. Also consider blinking cursors, rotating carousels, live clocks, video, and personalized content when pixel comparisons vary.
5. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot is blank or mostly a loading shell | Capture happened after navigation but before app data rendered. | Wait for the specific result element or expected state; check whether an API request failed. |
networkidle never resolves |
Polling, analytics, streaming, or another persistent request keeps the network active. | Use a target locator/assertion. If useful, wait for a less strict navigation milestone first. |
| Wait times out although the page looks loaded | Selector is wrong, element is inside a frame, hidden, or has different text. | Inspect the DOM, use the correct frame locator, and wait for the actual visible state. |
| Screenshot misses images lower on the page | Images are lazy-loaded and were never brought into view. | Scroll through the page, then wait for important images to complete before full-page capture. |
| Screenshot differs between runs | Animation, dynamic data, fonts, or layout shifts continue during capture. | Wait for a semantic readiness marker; control animations for visual tests and stabilize viewport and test data. |
| Browser closes before the file is written | Capture was not awaited or cleanup ran too early. | Await navigation, waits, and screenshot calls; close the browser in a finally block. |
Selenium proceeds too early after get() |
Document readiness was mistaken for application readiness. | Add WebDriverWait for the target condition. |
6. Reliability, speed, and cost considerations
- Prefer condition-based waits. They proceed as soon as the needed state appears and fail with a useful timeout, whereas a fixed sleep always spends the full delay and can still be too short.
- Set bounded timeouts. A missing element should produce a diagnosable failure instead of hanging indefinitely. Choose a timeout based on the site and environment.
- Keep the signal narrow. Waiting for one meaningful heading or result is usually more reliable than waiting for every request or every image on a complex page.
- Make captures repeatable. Fix the viewport, locale, timezone, device scale, test data, and animation behavior when comparing screenshots.
- Plan for browser operations. Self-hosting means maintaining browser binaries, workers, concurrency limits, cleanup, and retry behavior. A screenshot API can avoid that browser setup, with usage charges depending on the service and plan.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server from ScreenshotNeo. One GET request returns an image or PDF; see the API documentation. For this page, a simple call is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot, and each of those steps can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed; response headers identify the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. Sign up free for 1,000 screenshots a month, with no card required.
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.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(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
Frequently asked questions
Is there a universal “page finished rendering” event?
No. Browser events describe document lifecycle or network conditions, while applications can continue updating afterward. Match the wait to the content the image needs.
Should I use a fixed timeout?
Use a fixed delay only when there is a known, bounded delay with no better signal. Prefer a locator or assertion so the capture proceeds when the target is ready.
Does a stable screenshot mean the page is correct?
No. Stable pixels help visual comparison, but the page could be consistently showing the wrong data. Assert important content separately.
Can I use networkidle at all?
It can be an additional clue on suitable pages, but persistent requests may prevent it and network quiet does not guarantee visual readiness. Playwright discourages it as a test readiness strategy.


