ScreenshotNeo

BlogHow-to

Why Are Lazy-Loaded Images Missing from My Website Screenshot API Capture?

Full-page capture may not scroll the live page, so lazy images never load. Learn how to trigger them, wait for them, and capture a reliable result.

By the ScreenshotNeo team4 October 20267 min read

Short answer: a full-page screenshot can include the entire document without scrolling the live browser viewport through it. Images using loading="lazy" and content activated by IntersectionObserver may therefore still be unloaded when capture begins. Scroll through the page first, then wait for the images or page-specific ready state you need before taking the screenshot.

The exact controls depend on the screenshot API and browser engine. The workflow below shows the underlying browser steps with Playwright, then gives a provider-neutral checklist for hosted APIs.

1. Why lazy-loaded images are missing

Lazy loading defers work until an image is near the visible area. A page may use the browser’s native loading="lazy" attribute, JavaScript with IntersectionObserver, or another scroll-triggered mechanism.

A full-page screenshot means the output covers the full scrollable document; it does not necessarily mean the browser scrolled through the page first. Playwright describes full-page capture as capturing the page as if it fit on a very tall screen. A Playwright issue documents lazy images and observer-driven content that can be missed when capture expands the page without moving the viewport. See the Playwright screenshots guide, Page API, and Playwright issue #40941.

Navigation completing is not proof that deferred images have loaded. A lifecycle wait tells you about navigation state, not whether every below-the-fold image or application component is ready. Playwright also cautions against treating networkidle as a general readiness signal in its Page API documentation.

2. Diagnose the page before changing capture code

  1. Check whether the missing images are below the initial viewport. If only lower-page images are absent, scroll activation is a likely cause.
  2. Inspect the image markup for loading="lazy", and inspect the page code for IntersectionObserver or other scroll handlers.
  3. Determine whether your capture tool scrolls the live viewport before producing a full-page image. Full-document output alone does not establish that it did.
  4. After a scroll pass, check the actual target images or an application-specific ready signal. Do not rely only on a navigation event.
  5. Repeat against the same API and browser engine used in production. Hosted providers can implement full-page capture differently.

3. Playwright: scroll, wait for images, then capture

This runnable Node.js example scrolls in viewport-sized steps so viewport-based loaders have a chance to activate. It then waits for image elements to finish loading, checks that they have a usable natural width, returns to the top, and writes a full-page screenshot.

import { chromium } from 'playwright';

const target = process.argv[2] ?? 'https://example.com';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1365, height: 900 } });

try {
  await page.goto(target, { waitUntil: 'domcontentloaded', timeout: 30_000 });

  // Move the live viewport down the document in increments.
  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, 150));
    }
    window.scrollTo(0, document.documentElement.scrollHeight);
    await new Promise(resolve => setTimeout(resolve, 300));
  });

  // Wait for images present in the document to finish, subject to a deadline.
  await page.waitForFunction(() => {
    const images = [...document.images];
    return images.every(img => img.complete);
  }, { timeout: 20_000 }).catch(() => {
    console.warn('Some images did not finish before the image wait timed out.');
  });

  const imageState = await page.locator('img').evaluateAll(images =>
    images.map(img => ({
      src: img.currentSrc || img.src,
      complete: img.complete,
      width: img.naturalWidth
    }))
  );
  const failed = imageState.filter(img => !img.complete || img.width === 0);
  if (failed.length) {
    console.warn(`${failed.length} image(s) are incomplete or have no decoded width.`);
  }

  await page.evaluate(() => window.scrollTo(0, 0));
  await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
  await browser.close();
}

Install and run it with npm install playwright, npx playwright install chromium, then node capture.mjs https://your-site.example. The short per-step delay is a starting point, not a guarantee: increase or replace it with a page-specific readiness condition if the site needs more time.

Wait for the images that matter

Waiting for every image can take too long on pages with many remote assets. If a particular image matters, use a selector and wait for that element to load. This example waits for the image’s load or error event and then verifies that it decoded with a nonzero natural width:

const hero = page.locator('img.hero-image');
await hero.waitFor({ state: 'attached', timeout: 10_000 });
await hero.evaluate(img => {
  if (img.complete) return;
  return new Promise((resolve, reject) => {
    img.addEventListener('load', resolve, { once: true });
    img.addEventListener('error', () => reject(new Error('Hero image failed to load')), { once: true });
  });
});
const width = await hero.evaluate(img => img.naturalWidth);
if (width === 0) throw new Error('Hero image loaded without usable image data');

Use a selector that identifies the actual content image, not a placeholder or skeleton. If the application exposes a reliable ready marker, waiting for that marker is often clearer than guessing a fixed delay.

4. Hosted screenshot APIs: what to configure

For a hosted service, look for documented controls for page interactions, scrolling, waiting, or scripts that run before capture. Do not assume a parameter name from another provider: the title does not identify a service, API version, or browser engine. One provider’s screenshot API documentation explicitly recommends scrolling before capture to trigger lazy-loaded content.

When choosing or configuring an approach, check four things: whether it moves the live viewport; whether it waits for the images or state you care about; whether it supports the page’s browser-dependent behavior; and whether the added scroll and wait fit your request timeout.

5. Common errors and fixes

Symptom Likely cause Fix
All images below the fold are blank The tool captured the document without scrolling the live viewport. Add an incremental pre-capture scroll pass, then wait for image readiness.
Some images appear, but others remain blank Those images load later, require a particular threshold or scroll position, or are controlled by application code. Scroll in smaller increments and wait on the specific image or application-ready selector.
The screenshot has placeholders or skeletons The page rendered its shell before deferred data or images arrived. Wait for the content-specific ready state; a navigation load state may be too early.
An image is marked complete but still appears broken The request may have failed; a completed image can still have naturalWidth equal to zero. Check naturalWidth, the image URL, browser console, and network errors.
The capture times out after adding waits The scroll pass and image waits exceed the provider’s overall time budget, or a resource never settles. Use a deadline, wait only for required assets, and avoid waiting for every network request to become idle.
The local Playwright version works but the API result does not The hosted service may use different scrolling behavior, browser versions, or capture timing. Check that provider’s current documentation and test its actual capture path.

6. Performance, reliability, and cost

Scrolling adds work because the page may fetch images, run observers, and execute scripts as each region enters view. Incremental movement is more reliable for viewport-triggered loaders than a single jump, but the right step size and wait depend on the page. There is no universal numeric performance cost: image count, page scripts, network conditions, browser, and provider timeout all affect it.

For reliability, use bounded waits, verify the images important to the output, and record which images failed. Prefer a page-specific ready signal when one exists. Avoid making an unbounded wait for all network traffic to stop, since analytics, long polling, or other requests can remain active.

For cost, check how your provider bills retries, failed captures, and longer-running requests. The research sources do not establish pricing or billing behavior for screenshot APIs generally, so verify the current terms of the service you use.

7. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. A single GET request returns an image or PDF. Its capture flow accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each of those steps can be turned off. Its documented parameters include full-page capture with lazy images loaded, waits, custom JavaScript, and CSS.

For a direct request, see the ScreenshotNeo API documentation. This cURL example saves a WebP screenshot:

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp

Python:

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)

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 Bun.write('shot.webp', res);

ScreenshotNeo says bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are never billed; responses identify the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents. The Free plan includes 1,000 shots a month without a card; paid plans start at $5 for 3,000 shots. Sign up for 1,000 free screenshots a month, with no card required.

8. FAQ

Does fullPage: true guarantee every image is loaded?

No. It controls the screenshot extent; it does not by itself prove that viewport-triggered loading ran or that images finished loading.

Should I always wait for networkidle?

No. It is not a universal signal that deferred page content is ready. Prefer waiting for the specific images or page state your capture requires.

What if images are inserted as CSS backgrounds?

They do not appear in document.images. Scroll to activate the relevant section, then wait for a page-specific signal or inspect the rendered element before capturing.

Why does the same URL produce different results?

Loading can depend on viewport size, browser behavior, network timing, and application state. Keep those capture conditions consistent and use bounded, content-specific waits.