ScreenshotNeo

BlogHow-to

Fix Playwright Screenshots That Miss Lazy-Loaded Images on Competitor Pages

Full-page capture sets the screenshot’s size; it does not guarantee lazy images have loaded. Scroll through the target area, verify images, then capture.

By the ScreenshotNeo team4 October 20268 min read

Playwright’s fullPage: true captures the full scrollable page, but it does not guarantee that every lazy-loaded image has been requested and rendered. Scroll through the page area you need in bounded steps, wait for the target images to load, verify that they have usable content, return to the intended capture position, and then take the screenshot.

This matters for competitor-page snapshots because browser-native lazy loading may defer an image until it is near the viewport, and the browser chooses that distance. Navigation finishing—or the window load event firing—is not proof that every offscreen image is ready. See MDN’s image loading documentation and lazy-loading guide.

1. Use bounded scrolling before capture

The reliable general workflow is to make the page expose the content you intend to capture, wait for the images you care about, and only then capture. A full-page screenshot controls the output extent; it should not be treated as a universal lazy-loading trigger. Playwright defines full-page capture as a screenshot of the full scrollable page in its Screenshots guide.

  1. Navigate and establish the viewport, cookies, and page state used for the comparison.
  2. Decide the capture boundary: the whole finite page, a section, or a defined number of items in an infinite feed.
  3. Scroll through that boundary in configurable increments. Pause between steps to give deferred requests and rendering time to complete.
  4. Check the expected images. Treat img.complete as a state signal, and when successful image content matters also check naturalWidth > 0.
  5. Return to the desired starting position, then take a viewport or full-page screenshot.

There is no universal scroll increment or wait duration for every site. Make both configurable and verify the pages being collected. A page may use native loading="lazy", JavaScript observers, nested scrolling containers, carousels, or image URLs that are assigned only after interaction; diagnose these as page-specific possibilities.

2. Runnable Playwright example in JavaScript

Install Playwright and its browser using the official installation instructions. Save this as capture.mjs and run it with node capture.mjs https://example.com. The loop uses a fixed viewport-sized step, bounded by the document height observed before scrolling; adjust the step and pause for the target site.

import { chromium } from 'playwright';

const targetUrl = process.argv[2];
if (!targetUrl) {
  throw new Error('Usage: node capture.mjs https://example.com');
}

const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });

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

  // Record a finite boundary. For infinite scroll, replace this with an
  // explicit item count or application-specific stopping condition.
  const pageHeight = await page.evaluate(() =>
    Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)
  );
  const viewportHeight = page.viewportSize().height;
  const step = Math.max(200, Math.floor(viewportHeight * 0.8));
  const pauseMs = 500;

  for (let y = 0; y < pageHeight; y += step) {
    await page.evaluate((scrollY) => window.scrollTo(0, scrollY), y);
    await page.waitForTimeout(pauseMs);
  }

  // Wait until all currently present img elements have finished attempting
  // to load. A failed image can also be complete, so inspect naturalWidth.
  await page.waitForFunction(() => {
    const images = [...document.images];
    return images.length > 0 && images.every((img) => img.complete);
  }, { timeout: 15_000 }).catch(() => {});

  const imageReport = await page.locator('img').evaluateAll((images) =>
    images.map((img) => ({
      src: img.currentSrc || img.src,
      complete: img.complete,
      naturalWidth: img.naturalWidth,
      naturalHeight: img.naturalHeight
    }))
  );
  const unusable = imageReport.filter((img) => !img.complete || img.naturalWidth === 0);
  if (unusable.length) {
    console.warn('Images not confirmed usable:', unusable);
  }

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

The example deliberately reports image problems rather than hiding them. Decide whether your collection should fail when an expected image is unavailable, retry, or save the screenshot with a warning. For strict runs, maintain selectors for the expected images and assert those specific elements instead of requiring every decorative image on the page to succeed.

3. Check image readiness deliberately

MDN documents HTMLImageElement.complete as a way to inspect whether an image has finished loading. It does not by itself prove that the image loaded successfully, so pair it with a content check such as positive naturalWidth when a broken image must be detected. See the MDN <img> reference.

const status = await page.locator('img').evaluateAll((images) =>
  images.map((img) => ({
    src: img.currentSrc || img.src,
    complete: img.complete,
    usable: img.complete && img.naturalWidth > 0
  }))
);
console.table(status);

For a known key image, wait on that image rather than the entire document:

const hero = page.locator('.product-gallery img').first();
await hero.waitFor({ state: 'attached' });
await hero.evaluate((img) => {
  if (img.complete) return;
  return new Promise((resolve) => {
    img.addEventListener('load', resolve, { once: true });
    img.addEventListener('error', resolve, { once: true });
  });
});
const usable = await hero.evaluate((img) => img.complete && img.naturalWidth > 0);
if (!usable) throw new Error('Expected product image did not load');

Use an application-specific condition if the page replaces a data-src value, reveals images through an observer, or renders image cards only after a request. The correct condition depends on the page’s DOM and behavior; generic scrolling cannot guarantee content that the page never inserts.

4. Viewport versus full-page capture

Capture Use it for What to verify
Viewport Consistent snapshots at a chosen scroll position Scroll to the target area, verify visible images, and capture without full-page expansion.
Full page A record of the finite scrollable document Trigger the relevant deferred content first; check that document extent remains stable during capture.

For a full-page image, return to the top if the screenshot should begin there. If the site uses infinite scrolling, define a stopping point such as a known section or item count. Repeatedly chasing the bottom can keep expanding the document and make both runtime and output size unpredictable.

5. cURL, Python, and Node.js using ScreenshotNeo

If the goal is a screenshot rather than browser automation itself, ScreenshotNeo provides a website screenshot API and MCP server. Its API options include full-page capture with lazy images loaded. Use the API documentation for the available request parameters: ScreenshotNeo API docs.

cURL

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 import('node:fs/promises').then(({ writeFile }) =>
  writeFile('shot.webp', Buffer.from(await res.arrayBuffer()))
);

Replace the example target with the page you are authorized to capture. ScreenshotNeo also supports selector capture, viewport and device presets, retina scale, output formats, custom waits, cookies and headers, and other capture controls; see the docs for parameter details.

6. Or skip the browser setup

Use the ScreenshotNeo call above when you need a screenshot without maintaining a Playwright browser flow. Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots per 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.

7. Troubleshooting

Symptom Likely cause Fix
Full-page shot still has empty image areas Capture extent was expanded, but deferred loading was not triggered for those areas. Scroll the relevant regions first, pause for loading, inspect expected images, then capture.
Navigation completed but images are absent Offscreen lazy images may not delay the window load event. Use targeted scrolling and image readiness checks rather than treating navigation completion as proof.
complete is true but the image looks broken The request may have failed; completion is not a success guarantee. Check naturalWidth > 0 and inspect the image request or console for the failing URL.
Some cards never appear while scrolling The page may require a nested scroller, interaction, or app-specific condition; this is a diagnostic hypothesis. Inspect the actual scroll container and DOM changes. Scroll that container or wait for the site-specific card condition.
The scroll loop grows forever The page may append content on reaching the bottom. Set a maximum item count, section boundary, or scroll distance and stop there.
The screenshot intermittently misses images Loading and rendering time varies, or content changes during capture. Use a relevant image condition, make pauses configurable, and record which expected images failed instead of relying on one fixed sleep.
networkidle never occurs or is misleading Background connections can keep traffic active, while quiet network traffic does not prove the required image state. Prefer a page or image condition. Playwright documents networkidle as discouraged for tests and recommends assertions for readiness in the Page API.

8. Performance, reliability, and cost

Scrolling the entire document adds browser work and wait time. If the comparison only needs a product gallery or a few sections, bound the scan to those sections and assert only their images. A viewport capture is smaller and more consistent when the comparison needs one fixed view; a full-page image is useful for broad coverage but may be very tall and costly to store or process.

For repeatable collection, keep viewport, scroll step, pause, timeout, and stop condition explicit in configuration. Save the target URL and image-status report with the artifact so missing content is visible during review. On retries, retry only known transient failures and avoid unbounded loops on pages that append content as the visitor scrolls.

There is no universal delay that makes every site reliable. Network conditions, JavaScript behavior, nested scroll regions, and changing page content differ. Use verified conditions for important images and treat unexplained missing assets as a capture failure or a review warning according to the needs of your comparison workflow.

9. Frequently asked questions

Does fullPage: true trigger lazy loading?

Do not rely on it to do so. It requests a full scrollable-page capture, while lazy-loading behavior is tied to image proximity or site-specific logic.

Should I wait for every image on the page?

Only if every image is part of the required artifact. For a targeted comparison, wait for and validate the expected images so unrelated broken or decorative assets do not hold up the job.

What if the page has infinite scroll?

Set a finite collection boundary, such as a section or item count. An endlessly growing document has no dependable natural bottom for a full-page workflow.

Is a quiet network enough to capture?

No. A quiet network does not establish that the specific images you need have rendered. Check those image elements or a page-specific readiness condition.

Sources