ScreenshotNeo

BlogHow-to

Why Are Images Missing in a Headless Chrome Screenshot?

Images can be missing because capture starts too early, lazy loading has not run, or requests failed. Diagnose image state and wait for what the page needs.

By the ScreenshotNeo team4 October 20268 min read

Images are usually missing from a headless Chrome screenshot because capture happens before they are fetched or rendered, lazy-loaded images have not been triggered, or an image request failed or was blocked. A navigation event such as load or a generic network-idle wait is only a starting point: inspect the image element and its request, trigger offscreen images when needed, and wait for the specific images that must appear.

There is no single root cause without the page, browser version, capture command, and network log. The workflow below separates timing, lazy loading, failed requests, interception, and display problems.

1. Check whether the image exists and is ready

For an ordinary <img>, inspect its src, currentSrc, loading, complete, and naturalWidth values. A zero naturalWidth means the browser has no intrinsic image data available. complete alone is not proof of success: a broken image can also be complete. See MDN’s documentation for complete and naturalWidth.

const imageState = await page.evaluate(() =>
  [...document.images].map((img) => ({
    alt: img.alt,
    src: img.src,
    currentSrc: img.currentSrc,
    loading: img.loading,
    complete: img.complete,
    naturalWidth: img.naturalWidth,
    naturalHeight: img.naturalHeight,
  }))
);
console.table(imageState);

If the image is absent from this list, the page may create it later with JavaScript, or the visual may instead be a CSS background, canvas, SVG, or another element. Inspect the relevant DOM and styles in those cases; an img query cannot diagnose every kind of visual asset.

2. Use a page-specific wait in Puppeteer

Use a navigation lifecycle condition to get the page started, then wait for the actual images needed by the screenshot. Puppeteer’s screenshot guide demonstrates taking a screenshot after navigation; its server-rendering guidance explains that networkidle0 means 500 ms with no network requests and cautions that lazy-loaded pages can need more time. Network idle does not prove visual readiness.

Install Puppeteer with npm install puppeteer. Save the following as capture.cjs and run node capture.cjs https://example.com. It scrolls through the document to trigger common viewport-based lazy loading, returns to the top, waits for image fetches and decoding, reports images that remain unavailable, and writes a PNG.

const puppeteer = require('puppeteer');

(async () => {
  const url = process.argv[2];
  if (!url) throw new Error('Usage: node capture.cjs https://example.com');

  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
    page.setDefaultNavigationTimeout(60000);
    await page.goto(url, { waitUntil: 'domcontentloaded' });

    // Trigger common loading="lazy" behavior by bringing page regions near the viewport.
    await page.evaluate(async () => {
      const step = Math.max(300, Math.floor(window.innerHeight * 0.8));
      for (let y = 0; y < document.documentElement.scrollHeight; y += step) {
        window.scrollTo(0, y);
        await new Promise((resolve) => setTimeout(resolve, 100));
      }
      window.scrollTo(0, 0);
    });

    // Wait for image loading to settle. Failed images are reported separately.
    const result = await page.evaluate(async () => {
      const images = [...document.images];
      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.complete && img.naturalWidth > 0 && img.decode) {
          try { await img.decode(); } catch (_) { /* Include decode failures in the report below. */ }
        }
      }));
      return images.map((img) => ({
        src: img.currentSrc || img.src,
        complete: img.complete,
        naturalWidth: img.naturalWidth,
        naturalHeight: img.naturalHeight,
      }));
    });

    const unavailable = result.filter((img) => !img.complete || img.naturalWidth === 0);
    if (unavailable.length) console.error('Images unavailable at capture:', unavailable);

    await page.screenshot({ path: 'screenshot.png', fullPage: true });
    console.log('Saved screenshot.png');
  } finally {
    await browser.close();
  }
})().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

This is a practical baseline, not a universal page-ready detector. A page can insert or replace images after the evaluation begins, lazy loading can use an application-specific observer, and a failed image wait can otherwise stall indefinitely. For production, add a bounded image wait and a site-specific readiness condition where available. For one known image, target it by selector and wait for a successful state:

await page.waitForFunction(() => {
  const img = document.querySelector('.hero img');
  return img && img.complete && img.naturalWidth > 0;
}, { timeout: 15000 });

Set the selector to the image that matters on the target page. If the page exposes a reliable application-owned ready marker, waiting for that marker is often clearer than guessing from all network activity.

3. Trigger lazy loading before capture

Images marked loading="lazy" may not be requested until they approach the viewport. The window load event does not guarantee those deferred images have loaded. Scroll the target region into view, allow its requests to start, then wait for the corresponding image state before capturing. Browser lazy-loading behavior is described by MDN’s img loading reference.

await page.locator('.article-image').scrollIntoViewIfNeeded();
await page.waitForFunction(() => {
  const img = document.querySelector('.article-image');
  return img && img.complete && img.naturalWidth > 0;
}, { timeout: 15000 });

For a full-page capture, scrolling through the document before waiting is useful for common viewport-triggered lazy loaders. Some sites load only after a particular interaction or require several scroll cycles as new content is appended. Repeat the trigger and wait for the page’s own content condition where necessary.

4. Inspect network requests and interception

Use DevTools Network or Puppeteer request and response events to find whether image requests were sent and what happened to them. A missing request points toward deferred loading, JavaScript that has not assigned a source, or an element that has not been created. A failed response points toward a URL, access, server, or browser network problem.

page.on('requestfailed', (request) => {
  if (request.resourceType() === 'image') {
    console.error('Image request failed:', request.url(), request.failure()?.errorText);
  }
});
page.on('response', (response) => {
  if (response.request().resourceType() === 'image' && !response.ok()) {
    console.error('Image response:', response.status(), response.url());
  }
});

If request interception is enabled, check every handler path. An image can disappear because the handler aborts it or never continues it. Puppeteer’s server-rendering example intentionally blocks image requests as an optimization for markup-only output; that configuration is unsuitable when the screenshot must contain images. Review the Puppeteer network interception guide.

5. Choose the right capture wait

Approach Good for Limit
Chrome CLI --timeout Quick manual captures A maximum wait before capture, not proof that images are ready. See Chrome Headless documentation.
Puppeteer navigation wait General automated capture load, networkidle0, or networkidle2 may not cover lazy images or page-specific rendering. See the Puppeteer screenshot guide.
Selector or image-state wait Known page and target content Requires the right selector and a timeout or fallback for failures.
Page-owned ready signal Applications you control Requires the application to expose a trustworthy signal.

Prefer an explicit condition for the content in the screenshot. Use a delay only as a bounded allowance after a trigger, not as the sole evidence that an image loaded.

6. Common symptoms and fixes

Symptom Likely area to inspect Next step
Image element is absent Client-side rendering, wrong selector, page state Wait for the code that creates it or inspect the page’s ready marker.
src is empty or unexpected JavaScript source assignment or responsive source selection Inspect src and currentSrc after the relevant script runs.
No image request appears Lazy-load trigger or source assignment Scroll the image into view and observe whether a request starts.
Request fails URL, server response, authentication, referrer policy, network Read the failed request details and response status; reproduce with the same browser context.
All images fail only in automation Request interception or blocking rules Ensure image requests are continued and no resource-type rule blocks them.
Request succeeds, but screenshot is blank Decode, CSS visibility, layout, clipping, capture bounds Check dimensions and computed style, then confirm the image is inside the viewport or full-page capture area.
Broken-image icon or no intrinsic data Invalid or unsupported source, corrupt data, failed fetch Inspect browser console and network details; verify the image URL and response body.

These clues narrow the search; without a reproduction they do not establish which cause applies to a particular page.

7. Make captures reliable and efficient

  • Wait for the required content, not every possible request. Analytics, polling, and long-lived connections can make network-idle waits slow or unreliable. A target image or application-ready selector is more specific.
  • Set time bounds. Navigation, selector, and image waits should have finite timeouts. On timeout, log image state and request failures so the capture is diagnosable instead of hanging.
  • Avoid needless full-page scrolling. For a viewport screenshot, trigger only images in that viewport. For full-page output, scroll in measured increments and allow newly revealed content to request its assets.
  • Keep the capture context consistent. Viewport size, device scale factor, cookies, headers, and authentication can affect which source is selected and whether a server allows the request.
  • Treat retries carefully. Retry transient navigation or network failures with a small bounded policy. A deterministic bad URL, denied request, or blocked image will not be fixed by repeating the identical capture.
  • Control costs in your own browser service. Reusing a browser process can reduce startup overhead, but isolate page contexts and close pages; bound concurrency and memory use for large full-page captures.

8. Or skip the browser setup

If you do not want to maintain browser launch, lazy-load triggers, and image readiness checks, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF; its API documentation lists the capture options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits cost nothing, with page verdict and billing details in response headers. Its MCP server gives AI agents the take_screenshot, get_page_info, and capture_pdf tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.

Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.

FAQ

Does networkidle0 wait for every image?

No. It describes a period without network requests, not successful decoding or readiness of every image. Lazy images may not have requested their files yet.

Why is complete true while the image is broken?

complete can be true when fetching has finished unsuccessfully. Check naturalWidth and the network result as well.

Does a screenshot API remove the need to check a particular image?

For a page where a specific asset is mandatory, verify the resulting capture or use an API’s page and response diagnostics. A capture service does not make a broken source URL or inaccessible asset valid.