October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk8 min

How to Wait for Client-Side Images to Load Before a Puppeteer Screenshot

A network-idle event is not proof that screenshot images are ready. This guide shows a Puppeteer sequence for lazy-loaded, decoded images, element captures, timeouts, failures, and an API alternative.
Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

  1. Navigate with a sensible readiness checkpoint.
  2. Trigger lazy-loaded content that lies outside the viewport.
  3. Wait for the images in the capture area to load and decode.
  4. 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Site-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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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: complete may be true while naturalWidth is zero.
  • Decode rejection: the response may be present but malformed or not decodable; treat it separately from a network error.
  • CSS backgrounds: document.images does not include images loaded through background-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

  1. Confirm the capture geometry. Log viewport size, document height, and whether fullPage or an element handle is being used.
  2. Inspect image state. In DevTools or page.evaluate, print currentSrc, complete, naturalWidth, and naturalHeight.
  3. Check lazy-loading triggers. Scroll the exact region and verify that currentSrc changes from a placeholder to the real URL.
  4. Capture failures. Listen for page console and request failures, and include the resource URL in logs.
  5. Repeat after hydration. If a framework inserts images late, wait for its ready selector and rediscover the image list.
  6. 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.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

One 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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. Shenzhen desk3 min
    HONOR Expands Beyond Smartphones With Humanoid Robot RevealHONOR said it unveiled its first humanoid robot at MWC 2026 and named shopping assistance, workplace inspections, and supportive companionship as intended uses. Later Robotics D1 claims and a reported…
  2. Cupertino desk5 min
    Apple Unveils AirPods Max 2: The Upgrade That Should Have Happened Years AgoAirPods Max 2 adds H2-powered audio features and Apple claims up to 1.5× more effective ANC, but its design, Smart Case, and 20-hour battery rating are unchanged. Wired lossless audio…
  3. Cupertino desk4 min
    Apple’s OLED Touch MacBooks Are Coming—but the Dynamic Island Is the Real GambleApple has not announced an OLED touchscreen MacBook, but reports point to high-end models arriving in late 2026 or early 2027. The reported Mac Dynamic Island could be useful, but…
Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Crashes, No Sound, or Screen Glitches?Free driver scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.