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
browser automation

How to Fix Lazy-Loaded Images Missing from Puppeteer Screenshots

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

Scroll the page to trigger its lazy loader, wait for the images that matter to report complete with a non-zero naturalWidth, then call page.screenshot(). A full-page screenshot captures pixels; it does not guarantee that offscreen images were ever requested. Network-idle waiting helps, but it cannot prove that an image which never entered the viewport was loaded.

Why a full-page screenshot can contain blank image areas

Puppeteer’s screenshot API captures the current rendered document. Many sites defer image requests until an image is visible, or nearly visible, to reduce initial work. Native loading="lazy", IntersectionObserver, and framework-specific loaders all use some form of viewport visibility. Google’s guidance describes visibility-triggered loading and IntersectionObserver as common patterns (Google Search Central).

That creates two separate stages:

  • Loading: the page’s code must request the image and the browser must decode it.
  • Capture: Puppeteer must rasterize the resulting pixels.

fullPage: true changes the capture area; it is not a command to run every lazy-loading trigger. Likewise, waitForNetworkIdle() can return while an offscreen image has never been requested, so treat it as supporting evidence rather than a readiness guarantee (Puppeteer Page API).

The reliable Puppeteer workflow

1. Navigate without racing the application

Use domcontentloaded for a predictable starting point, then perform any cookie acceptance, login, viewport, or consent setup your target requires.

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.

2. Scroll in viewport-sized increments

Scroll progressively from the top toward the document bottom. Incremental movement gives intersection-based loaders a chance to observe each region. A single jump to the bottom can skip custom intermediate triggers. The exact increment and pause are site-specific; start around 80% of the viewport height and adjust when diagnostics show late loading.

3. Wait for the images that should be present

Define the expected set instead of blindly waiting for every <img>. Tracking pixels, intentionally empty images, broken URLs, and images inserted later can make a page-wide predicate wait forever. For ordinary content images, require both img.complete and img.naturalWidth > 0.

4. Capture only after readiness is established

Use page.screenshot({ fullPage: true }) for the document or an element handle for one component. Puppeteer documents that ElementHandle.screenshot() tries to scroll a hidden target into view by default (Puppeteer Screenshots guide). That behavior helps the selected element, but it does not load every offscreen image in a page-wide capture.

Runnable JavaScript example

This script scrolls in steps, waits briefly after each step, optionally waits for network quietness, checks the relevant images, and writes a full-page PNG. Replace the URL and selector policy for your site.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const puppeteer = require('puppeteer');

(async () => {
  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/catalog', {
    waitUntil: 'domcontentloaded',
    timeout: 60000
  });

  // Accept consent or perform other page setup here if required.
  await page.evaluate(async () => {
    const step = Math.max(300, Math.floor(window.innerHeight * 0.8));
    let lastHeight = 0;
    for (;;) {
      const height = document.documentElement.scrollHeight;
      for (let y = 0; y < height; y += step) {
        window.scrollTo(0, y);
        await new Promise(resolve => setTimeout(resolve, 150));
      }
      // Allow images or infinite-scroll content added at the bottom to appear.
      await new Promise(resolve => setTimeout(resolve, 300));
      const newHeight = document.documentElement.scrollHeight;
      if (newHeight <= height || newHeight === lastHeight) break;
      lastHeight = newHeight;
    }
    window.scrollTo(0, 0);
  });

  // Optional: useful after scrolling, but not a substitute for the image check.
  try {
    await page.waitForNetworkIdle({idleTime: 500, timeout: 10000});
  } catch (_) {
    // Long-polling or analytics may prevent network idle; continue to the explicit check.
  }

  await page.waitForFunction(() => {
    const images = [...document.querySelectorAll('img[data-screenshot="include"], img.content-image')];
    return images.length > 0 && images.every(img => img.complete && img.naturalWidth > 0);
  }, {timeout: 15000});

  await page.screenshot({path: 'page.png', fullPage: true});
  await browser.close();
})();

If your page has no stable class, inspect all images temporarily, then narrow the selector after identifying decorative, tracking, or intentionally optional elements:

const report = await page.evaluate(() => [...document.images].map((img, i) => ({
  index: i,
  src: img.currentSrc || img.src,
  dataSrc: img.getAttribute('data-src'),
  complete: img.complete,
  naturalWidth: img.naturalWidth,
  loading: img.loading
})));
console.table(report);

Do not interpret complete === true alone as success: a failed resource can also be complete with a zero natural width.

Choose the loading strategy that matches the page

Approach Best use Trade-off
Scroll and wait for readiness Native lazy loading or viewport-triggered JavaScript Closest to normal user behavior; requires a timeout and failure policy.
Set native images to eager You control markup and have confirmed loading="lazy" is the cause Simplifies native loading, but does not activate custom loaders.
Invoke the site’s loader Images use data-src, a framework store, or an application callback Most precise and often fastest, but implementation-specific.

Targeted native fallback

For markup that really uses native lazy loading, change only the images needed for the capture before waiting:

await page.evaluate(() => {
  for (const img of document.querySelectorAll('img.content-image')) {
    img.loading = 'eager';
  }
});

This does not copy a URL from data-src, run an observer callback, or fix a custom component. Prefer scrolling first, because it exercises the same trigger a visitor would use.

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

Custom attributes and dynamic insertion

Inspect data-src, data-srcset, srcset, and framework state. Some loaders copy a data attribute into src only after an observer callback; others insert an entirely new image node. In those cases, trigger the documented application event or scroll the exact container rather than assuming that changing loading is sufficient. Re-scan the DOM after scrolling if infinite-scroll code adds images.

Element screenshots versus full-page screenshots

An element capture can be appropriate when only one component matters:

const card = await page.waitForSelector('.product-card');
await card.screenshot({path: 'card.png'});

Puppeteer may scroll that hidden element into view automatically, as documented in its screenshots guide. Verify the element’s own images afterward; automatic scrolling of one target is not page-wide readiness. For long documents whose layout changes while images decode, compare a normal viewport screenshot after scrolling with the final full-page result. If full-page stitching still differs, capture viewport-sized segments after each segment is ready and combine them in your pipeline.

Diagnostics and fixes for common failures

The scroll loop runs, but requests never start

Confirm that the page, not an inner scroll container, is moving. If a catalog uses .results with overflow: auto, scroll that container and wait for its content. Check that a consent overlay, modal, or disabled script is not preventing the observer from running.

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

The readiness predicate never finishes

Log the failing images and their dimensions. Remove broken, optional, tracking, and intentionally empty images from the expected selector. Set a finite timeout and report failures rather than allowing a job to hang indefinitely.

Images keep their placeholder URLs

Look for data-src or data-srcset and inspect the page’s loader state. A custom loader may require a framework event, a particular scroll container, or a user interaction. Native eager mode cannot repair a URL that was never assigned.

Network idle says success, but pixels are blank

Network quiet means no qualifying requests were active during the idle window; it does not mean every expected image was requested. Check currentSrc, complete, and naturalWidth after the visibility trigger.

The page changes height during capture

Lazy decoding, web fonts, and infinite-scroll insertion can shift layout. Record scrollHeight after each pass, perform another pass when it grows, and wait for the selected images again before capturing. For especially unstable pages, capture settled viewport segments instead of relying on one stitched full-page operation.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

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

Only some formats fail

Inspect response errors, redirects, authorization, and content security policy. A successful HTML navigation does not imply that every image host is reachable from the browser context. Supply the required cookies or headers before scrolling and use a realistic viewport and user agent when the site varies content by device.

Timeouts, performance, and reliability

  • Use bounded waits: choose a navigation timeout, per-scroll pause, and image-readiness timeout. On timeout, save a diagnostic screenshot and the image report.
  • Scroll only as far as needed: if the capture ends at a known component, stop after that component has entered view. Full-document jobs must account for content added while scrolling.
  • Keep selectors explicit: a business selector such as img.content-image is more reliable than treating every image, including analytics pixels, as required.
  • Reuse a browser carefully: reusing a Chromium process reduces startup cost, but create an isolated page or context per job so cookies, scroll position, and service-worker state do not leak.
  • Capture evidence: log the final URL, viewport, document height, number of expected images, failed URLs, and timeout reason. This makes intermittent failures diagnosable.

There is no universal delay or scroll increment. Start with the example values, measure the target’s behavior, and increase waits only when the page demonstrably needs them.

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

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP, or PDF, while its clean-shot flow accepts consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Each step can be disabled when you need the original page state.

For a direct capture:

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 API documentation for all parameters. The same request in Python:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import requests
r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
require('fs').writeFileSync('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo reports X-Page-Verdict and X-Billed headers. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; only clean shots are billed. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients. Options include full-page lazy-image loading, CSS-selector element capture, dark mode, device presets, retina scale, custom CSS and JavaScript, clicks, selector or network-idle waits, blocked resources, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, async webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. Parameter names used by other screenshot APIs also work.

The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; higher plans are Growth ($15/15,000), Pro ($39/60,000), Scale ($99/250,000), and Business ($249/1,000,000). Yearly billing gives two months free, and every feature is on every plan. Create a free ScreenshotNeo account to try 1,000 screenshots a month without a card.

FAQ

Does fullPage: true force lazy images to load?

No. It expands the capture area but does not replace the page’s visibility trigger.

Should I always disable lazy loading?

No. Reproducing the site’s real trigger is safer; eager mode is a targeted fallback for confirmed native lazy markup.

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

What is the minimum success check?

For each required image, verify that it is complete and has a non-zero natural width, then capture.

Why can an image be complete but still unusable?

Failed resources can report completion with a zero natural width, and a placeholder can be complete while the real URL remains in a data attribute.

Frequently Asked Questions

Does fullPage: true force lazy images to load?

No. It expands the capture area but does not replace the page’s visibility trigger.

Should I always disable lazy loading?

No. Reproducing the site’s real trigger is safer; eager mode is a targeted fallback for confirmed native lazy markup.

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

What is the minimum success check?

For each required image, verify that it is complete and has a non-zero natural width, then capture.

Why can an image be complete but still unusable?

Failed resources can report completion with a zero natural width, and a placeholder can be complete while the real URL remains in a data attribute.

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 *

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

Read next

Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
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.