How to Wait for Images to Load Before a Playwright Screenshot
Wait for the images your screenshot needs, check whether they loaded successfully, and handle lazy loading before capturing with Playwright.
Use Playwright’s page.waitForFunction() to wait until the images needed in your screenshot have finished loading, then capture with page.screenshot(). For a successful image rather than merely a finished request, check that image.complete is true and image.naturalWidth > 0. If the page uses lazy loading, trigger it first by scrolling through the capture area. Keep the wait bounded and report failed or still-pending images instead of assuming that a longer delay will fix them.
1. Wait for images, then capture
This runnable Node.js example waits for every image currently in the document and treats broken images as failures. Install Playwright with npm install playwright; install its browser with npx playwright install chromium. Save the code as screenshot.js, then run node screenshot.js https://example.com.
const { chromium } = require('playwright');
async function main() {
const url = process.argv[2] || 'https://example.com';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
try {
await page.goto(url, { waitUntil: 'load', timeout: 30_000 });
const failures = await page.waitForFunction(() => {
const images = [...document.images];
return images.every(image => image.complete && image.naturalWidth > 0);
}, { timeout: 15_000 }).then(() => []).catch(async error => {
return await page.evaluate(() => [...document.images]
.filter(image => !image.complete || image.naturalWidth === 0)
.map(image => ({
src: image.currentSrc || image.src,
complete: image.complete,
naturalWidth: image.naturalWidth
})));
});
if (failures.length) {
throw new Error(`Images did not load successfully: ${JSON.stringify(failures)}`);
}
await page.screenshot({ path: 'page.png', fullPage: true });
console.log('Saved page.png');
} finally {
await browser.close();
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
The predicate is evaluated in the page. complete becomes true when an image has either loaded or failed, so it alone does not establish success. Add the naturalWidth > 0 check when broken or empty images should fail the capture. If missing assets are acceptable for your use case, wait for complete and log failures separately instead of throwing.
2. Choose the readiness condition that matches the page
All document images
For a page where every image matters, use document.images and require all entries to finish. This is the simplest condition, but it can wait on irrelevant tracking pixels or images outside the intended screenshot.
await page.waitForFunction(
() => [...document.images].every(image => image.complete),
{ timeout: 10_000 }
);
Only successfully loaded images
await page.waitForFunction(
() => [...document.images].every(
image => image.complete && image.naturalWidth > 0
),
{ timeout: 15_000 }
);
Use this when the screenshot is invalid if any expected image is broken. If some images are optional, define the relevant set more narrowly rather than making every site image a hard requirement.
Images inside a section
A CSS selector can scope the check to a content area. This example waits for images below main only:
await page.waitForFunction(() => {
const images = [...document.querySelectorAll('main img')];
return images.length > 0 && images.every(
image => image.complete && image.naturalWidth > 0
);
}, { timeout: 15_000 });
The images.length > 0 guard matters if an empty section should not count as ready. If no images is a valid page state, remove the guard.
Images that appear after hydration
A one-time list of image elements can become stale if the application inserts or replaces images after navigation. A predicate that re-queries the DOM on every evaluation, as above, handles additions while the wait is active. If the page changes the image source only after a user action or a later route transition, perform that action first, then wait for the final expected content.
3. Trigger lazy loading before the check
Offscreen images may not be requested until they approach the viewport. A full-page screenshot captures the scrollable page, but do not treat that option by itself as proof that every below-the-fold lazy image was requested. Scroll through the page before waiting. This helper advances the main document in viewport-sized steps, then returns to the top:
async function triggerLazyImages(page) {
await page.evaluate(async () => {
const step = Math.max(1, window.innerHeight);
for (let y = 0; y < document.documentElement.scrollHeight; y += step) {
window.scrollTo(0, y);
await new Promise(resolve => requestAnimationFrame(resolve));
}
window.scrollTo(0, 0);
});
}
await triggerLazyImages(page);
await page.waitForFunction(() => [...document.images].every(
image => image.complete && image.naturalWidth > 0
), { timeout: 20_000 });
await page.screenshot({ path: 'full-page.png', fullPage: true });
Validate this behavior on the target application. A page may lazy-load images in a nested scroll container rather than the document; scroll that container instead. Pages that continuously append content need an application-specific stopping condition, or the scroll loop may keep discovering more content.
4. Navigation waits and screenshot stability
page.goto() waits for the load state by default: the document’s load event has fired. domcontentloaded is an earlier milestone, while commit means the response arrived and document loading began. These states describe navigation progress, not whether a particular set of images succeeded. [Playwright Page API]
Playwright defines networkidle as no network connections for at least 500 ms, but discourages using it for tests. Network quiet does not identify which images the screenshot needs, and applications may defer or continue work independently. Prefer an explicit readiness condition tied to the page content. [Playwright Page API: waitUntil]
For visual regression tests, Playwright Test’s expect(page).toHaveScreenshot() waits for two consecutive screenshots to match before comparing against the expected snapshot. That helps with visual stability, but does not guarantee a specific image loaded successfully. Pair it with an image readiness check when image presence matters. Keep browser, operating system, and rendering settings consistent because they can affect screenshot output. [Playwright visual comparisons]
5. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The wait times out | An image request failed, a source is swapped after hydration, or an offscreen lazy image was never triggered. | On timeout, collect each image’s currentSrc, complete, and naturalWidth. Trigger lazy loading, wait after the state-changing action, and verify the image URL can load. |
complete is true but the image is blank |
complete also covers failed loads; it is not a success check. |
Require naturalWidth > 0 for images that must render, and report failures explicitly. |
| The screenshot omits images near the bottom | The page has not requested offscreen lazy images, or they live in a nested scroll area. | Scroll the document or relevant container before checking readiness. Validate that scrolling causes the page to request the images. |
| Waiting for all images never finishes | An irrelevant image is broken, an image is continuously replaced, or the application keeps adding content. | Scope the selector to required content, set a finite timeout, and inspect the changing image set instead of raising the timeout blindly. |
networkidle passes but images are absent |
Network inactivity did not establish that the desired image set was loaded. | Wait for the relevant image predicate or an application-specific readiness marker. |
| The screenshot changes between runs | Fonts, animations, dynamic content, browser version, OS, or rendering conditions differ. | Use a consistent environment, wait for required content, and disable or stabilize animation where the test setup requires it. |
6. Performance, reliability, and cost
Waiting for every image can add latency and make captures vulnerable to unrelated broken assets. Prefer the smallest image set that represents the screenshot’s requirements, use a finite timeout, and collect failure details so a timeout has diagnostic value. Scrolling a long page to trigger lazy loading adds work; only do it when the capture includes below-the-fold content. A fixed sleep is not a readiness condition: it can waste time on fast pages and still capture too early on slow ones.
For a Playwright-based capture, reliability comes from matching the wait to the page’s behavior: trigger the right lazy-load mechanism, wait for the final elements, distinguish completion from success, and keep the capture environment stable. Per-capture browser cost depends on your own infrastructure and workload; this guide makes no benchmark or pricing claim.
7. Or skip the browser setup
If you need a screenshot without managing Playwright and browser readiness code, ScreenshotNeo is a website screenshot API and MCP server. Its API documentation covers capture options. A basic cURL request is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Equivalent Python:
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)
Equivalent Node.js:
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 require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed; response headers report the page verdict and billing status. AI agents can use its MCP server 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 screenshots.
Sign up free for 1,000 screenshots a month, with no card required.
8. FAQ
Does page.screenshot() wait for images?
Do not rely on the screenshot call as an image-readiness check. Wait for the page condition your capture requires, then call it.
Should I fail the capture when an image is broken?
For visual baselines, usually yes if the image is expected content. For resilience checks or pages with optional media, record the failure and continue according to the test’s purpose.
Is a fixed delay ever useful?
A short delay can help with a known animation or application transition, but elapsed time alone does not prove that the required images loaded. Pair any delay with a content check.


