Wait for each screenshot-relevant image to finish loading and decoding, then capture. A networkidle2 navigation checkpoint (or page.waitForNetworkIdle()) is useful, but it only describes network activity. It does not prove that every image is requested, successfully loaded, or decoded for rendering. For full-page captures, first trigger offscreen lazy-loaded images, then check their DOM state and apply an explicit timeout and failure policy.
The reliable sequence
A robust Puppeteer workflow has four distinct stages:
- Navigate with a sensible readiness checkpoint.
- Trigger lazy-loaded content that lies outside the viewport.
- Wait for the images in the capture area to load and decode.
- Take the page or element screenshot only after the check completes.
The following script is a complete starting point for Puppeteer 25.x (the current documentation page surfaced version 25.12.0 on September 29, 2026; verify API details when using a later release).
Full-page screenshot with image readiness checks
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({headless: true});
const page = await browser.newPage();
await page.setViewport({width: 1440, height: 900, deviceScaleFactor: 1});
await page.goto('https://example.com', {
waitUntil: 'networkidle2',
timeout: 60000
});
// Trigger loading of content that starts near the viewport only.
await page.evaluate(async () => {
const step = Math.max(window.innerHeight * 0.8, 400);
for (let y = 0; y < document.documentElement.scrollHeight; y += step) {
window.scrollTo(0, y);
await new Promise(resolve => setTimeout(resolve, 150));
}
window.scrollTo(0, 0);
});
const result = await page.evaluate(async () => {
const images = [...document.images];
const failures = [];
await Promise.all(images.map(async (img, index) => {
if (!img.complete) {
await new Promise(resolve => {
const done = () => resolve();
img.addEventListener('load', done, {once: true});
img.addEventListener('error', done, {once: true});
});
}
if (img.naturalWidth === 0) {
failures.push({index, src: img.currentSrc || img.src, reason: 'load-or-image-error'});
return;
}
if (typeof img.decode === 'function') {
try {
await img.decode();
} catch {
failures.push({index, src: img.currentSrc || img.src, reason: 'decode-failed'});
}
}
}));
return {count: images.length, failures};
});
if (result.failures.length) {
console.warn('Images unavailable:', result.failures);
// Choose your policy: throw, retry, or accept a partial capture.
}
await page.screenshot({path: 'page.png', fullPage: true});
await browser.close();
The load listener resolves on both load and error so one broken resource cannot leave the Promise pending forever. The result records failures instead of silently treating them as success. In a production job, convert that policy into a deliberate decision: reject the screenshot when every image is required, retry transient failures, or continue when missing images are acceptable.
#1 Best Overall
Why network idle is not an image-ready signal
Puppeteer’s page.goto(..., {waitUntil: 'networkidle2'}) is a navigation checkpoint. page.waitForNetworkIdle() waits for network inactivity; its documented defaults are concurrency: 0 and idleTime: 500 milliseconds, and it waits at least for the configured idle period. Those settings say nothing about whether an image request ever started or whether downloaded bytes have been decoded.
An image may still be absent because:
- A lazy-loading attribute has deferred the request until the element approaches the viewport.
- JavaScript will insert or replace the image after the idle window.
- The request failed, returned an unusable response, or was blocked by a policy.
- Bytes arrived but the browser has not completed decoding for paint.
Use network idle as a broad checkpoint, then inspect each relevant HTMLImageElement. The browser’s complete property can be true for a broken image or an image with no source. Pair it with naturalWidth > 0; when available, await decode(), whose Promise resolves when image data is decoded and ready to render. MDN documents these states and the fact that decode() can reject.
Handling lazy-loaded images in a full-page capture
For a full-page screenshot, the document can be much taller than the viewport. Images marked loading="lazy", or images managed by an intersection observer, may not be requested until you scroll near them. The initial load event and a quiet network period can therefore occur while lower sections have no image requests at all.
Rank #2
Progressive scrolling
The example scrolls in viewport-sized increments, pauses briefly, and returns to the top. This is a generic trigger, not a guarantee: some sites use custom virtualized lists, sentinel elements, or a different threshold. After scrolling, recalculate document.images because an application can add nodes dynamically. If the page continues replacing images, run the readiness check again after the application reaches its own stable-state condition.
Do these 3 things before closing this tab:
1Scan for outdated or missing drivers - takes under a minute2Repair Windows errors before they cause bigger problems3Fix the driver behind crashes, sound loss and screen glitchesSite-specific alternatives
- Call the page’s documented “load more” or “render all” action before capture.
- Disable a known lazy-loading feature through test configuration rather than relying on artificial scrolling.
- Wait for an application-specific selector or status flag that means content assembly is complete, then perform the per-image check.
Do not assume the first snapshot of document.images is permanent. Modern frameworks can hydrate, virtualize, or replace nodes after your check starts.
Waiting for one element instead of the whole page
If you only need a chart, hero image, or card, limit waiting to the target region. Puppeteer’s element screenshot method scrolls the element into view when necessary. That scroll can itself trigger lazy loading, so obtain the handle, allow the scroll, and then check images inside the element before capturing.
Rank #3
const card = await page.waitForSelector('.product-card', {visible: true, timeout: 30000});
await card.evaluate(el => el.scrollIntoView({block: 'center'}));
await card.evaluate(async el => {
const images = [...el.querySelectorAll('img')];
await Promise.all(images.map(async img => {
if (!img.complete) {
await new Promise(resolve => {
img.addEventListener('load', resolve, {once: true});
img.addEventListener('error', resolve, {once: true});
});
}
if (img.naturalWidth > 0 && typeof img.decode === 'function') {
await img.decode().catch(() => {});
}
}));
});
await card.screenshot({path: 'card.png'});
For strict jobs, replace the empty decode catch with a failure record and throw when an image is mandatory. Scoping the check avoids waiting for unrelated advertisements or below-the-fold content.
Timeouts, failures, and dynamic pages
Set a deadline
There is no universal image timeout. Set one according to the target’s normal response time and the cost of a stuck capture. The listener pattern above should be wrapped in a deadline so a page that never emits either event cannot hold a worker indefinitely. Surface the URLs and indexes of images still pending when the deadline expires.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →Choose a failure policy
| Situation | Recommended action |
|---|---|
| Brand or legal artwork is required | Fail the job, log the URL, and retry once if the error appears transient. |
| Decorative or third-party content failed | Continue, but record a partial-image warning with the screenshot metadata. |
| Only a few images fail intermittently | Retry those resources or the page after a short, bounded delay. |
| The image list changes during rendering | Wait for an app-specific stable signal, then repeat discovery and readiness checks. |
Common edge cases
- Broken URL:
completemay be true whilenaturalWidthis zero. - Decode rejection: the response may be present but malformed or not decodable; treat it separately from a network error.
- CSS backgrounds:
document.imagesdoes not include images loaded throughbackground-image. Wait on a page-specific selector or computed-style/resource signal when those assets matter. - Service workers and caches: a cached response can make network activity look idle immediately; DOM readiness is still the deciding check.
- Animated formats: loading and decoding can succeed while the captured frame varies. Freeze animation with test CSS or capture at a known delay if deterministic output is required.
Debugging missing images
- Confirm the capture geometry. Log viewport size, document height, and whether
fullPageor an element handle is being used. - Inspect image state. In DevTools or
page.evaluate, printcurrentSrc,complete,naturalWidth, andnaturalHeight. - Check lazy-loading triggers. Scroll the exact region and verify that
currentSrcchanges from a placeholder to the real URL. - Capture failures. Listen for page console and request failures, and include the resource URL in logs.
- Repeat after hydration. If a framework inserts images late, wait for its ready selector and rediscover the image list.
- Inspect the output. A valid PNG can still be incomplete; compare the failed-image list with the screenshot region rather than trusting file creation alone.
If the page never reaches a stable state, narrow the capture to a known element, increase the deadline only when justified, or define a partial-capture policy instead of waiting forever.
Rank #4
Performance and reliability considerations
Waiting for every image increases capture time on image-heavy pages, but it prevents a fast, repeatable class of blank or partially rendered screenshots. Progressive scrolling adds work proportional to page height; use larger steps when the site’s lazy threshold permits and smaller steps when images load only very near the viewport. Element-scoped checks are usually cheaper than page-wide checks.
Keep navigation, lazy-load triggering, readiness, and screenshot timing separate in logs. That lets you distinguish a slow server from a decode failure or a page that keeps mutating. Use bounded retries, never an unbounded sleep. If reliability matters more than completeness, fail explicitly with the unavailable URLs rather than silently publishing a misleading image.
Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server when you do not want to maintain Puppeteer image-wait logic. Its capture process accepts cookie and consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
Windows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallOutdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchOne request returns an image or PDF:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo documentation for all options, including full-page lazy-image loading, CSS-selector element capture, device and viewport settings, retina scale, custom JavaScript and CSS, waits, request blocking, cookies and headers, caching TTL, signed links, asynchronous webhooks, bulk capture, and PDF controls.
Best Value
The same call in 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)
And 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}`);
The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Sign up free to try it without a card.
FAQ
Does networkidle2 guarantee that images are visible?
No. It is a network-activity checkpoint, not a per-image load and decode test.
Should a broken image make the screenshot fail?
Only if that image is required for your use case. Record failures and choose between retrying, rejecting, or accepting a partial capture.
Why does a full-page screenshot miss images below the fold?
Lazy-loading code may not request those images until scrolling makes them eligible. Trigger the relevant scroll or use a page-specific render-all mechanism before checking readiness.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




