ScreenshotNeo

BlogHow-to

How to screenshot a website with lazy-loaded images for documentation

Capture complete website screenshots by triggering lazy-loaded images before capture. This guide covers manual steps, Playwright, Puppeteer, troubleshooting, and an API option.

By the ScreenshotNeo team4 October 20269 min read

Direct answer: scroll through the page before taking a full-page screenshot. Lazy-loaded images may not be requested until they approach the viewport, so a page-load event or a full-page capture alone does not prove that every image has rendered. Scroll in overlapping steps, let newly triggered requests and rendering settle, check important images, then capture and inspect the result. MDN describes native lazy loading as deferring an image until it reaches a browser-calculated distance from the viewport; that distance is browser-defined, not a universal fixed threshold.

1. Why images are missing from full-page screenshots

Full-page capture controls how much of the document is included. It does not necessarily cause every site’s deferred content to load first. Playwright defines fullPage: true as capturing the full scrollable page, while native lazy loading can defer fetching an offscreen image. The browser’s load event is therefore not a reliable signal that every lazy image is ready.

Pages may also load content through custom JavaScript, replace image sources later, or append more content as you scroll. A fixed delay or network-idle condition can help with some pages, but neither proves that arbitrary application-specific loading has finished.

2. Manual browser workflow

  1. Open the page at the viewport size and browser state you need to document. Sign in or establish consent and responsive layout state first if those affect the page.
  2. Scroll down in overlapping steps through the document. Pause after each step so newly eligible images and sections can be requested and rendered.
  3. Watch for page-height growth. On infinite-scroll pages, reaching the initial bottom once may only reveal the next portion; continue until the intended content has appeared.
  4. Return to the top if your capture method starts at the current scroll position or requires a known starting point.
  5. Take a full-page screenshot, or capture segments when the page is virtualized or extremely long.
  6. Inspect the saved image at readable zoom. Look for blank image slots, omitted lower sections, duplicated sticky controls, and layout shifts.

If an image remains absent, inspect its request and element state instead of repeatedly changing screenshot dimensions. A failed or blocked request, a custom deferred source, or page-specific JavaScript may require a different fix.

3. Automate with Playwright (Node.js)

Install Playwright and its browser using the official Playwright installation guide. Save the following as capture.mjs; set TARGET_URL to the page you are documenting. This example scrolls incrementally, waits for each step, waits for image elements to finish or fail, returns to the top, and saves a full-page PNG. The pause is a starting point to tune for the target site, not a universal guarantee.

import { chromium } from 'playwright';

const url = process.env.TARGET_URL;
if (!url) throw new Error('Set TARGET_URL to the page URL');

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

  await page.evaluate(async () => {
    const pause = (ms) => new Promise(resolve => setTimeout(resolve, ms));
    let previousHeight = 0;
    let stablePasses = 0;
    for (let pass = 0; pass < 30 && stablePasses < 2; pass++) {
      const height = document.documentElement.scrollHeight;
      const step = Math.max(300, Math.floor(window.innerHeight * 0.8));
      for (let y = 0; y < height; y += step) {
        window.scrollTo(0, y);
        await pause(300);
      }
      await pause(500);
      const newHeight = document.documentElement.scrollHeight;
      stablePasses = newHeight === previousHeight ? stablePasses + 1 : 0;
      previousHeight = newHeight;
    }

    window.scrollTo(0, 0);
    await pause(300);
    await Promise.all([...document.images].map(img => {
      if (img.complete) return Promise.resolve();
      return new Promise(resolve => {
        img.addEventListener('load', resolve, { once: true });
        img.addEventListener('error', resolve, { once: true });
      });
    }));
  });

  const imageState = await page.locator('img').evaluateAll(images =>
    images.map(img => ({ src: img.currentSrc || img.src, complete: img.complete, naturalWidth: img.naturalWidth }))
  );
  const missing = imageState.filter(img => !img.complete || img.naturalWidth === 0);
  if (missing.length) console.warn('Images incomplete or failed:', missing);

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

Run it with TARGET_URL=https://example.com node capture.mjs. The loop has a pass limit to avoid hanging forever on pages that keep growing. Adjust the step delay and readiness checks for the site. For a key image, a stronger check is to wait for its selector and verify naturalWidth > 0. The script treats image errors as settled so one broken asset does not block the entire capture; it reports those images for inspection.

For reproducibility, keep the viewport, browser state, and capture date with the documentation. If the page relies on authentication, configure a Playwright browser context or storage state rather than placing credentials in source code.

4. Automate with Puppeteer

Puppeteer’s screenshot guide uses Page.screenshot() and demonstrates navigation with waitUntil: 'networkidle2'. Network idle can be a useful navigation condition, but it does not guarantee that images triggered by later scrolling or application logic are ready. Use the same scroll, wait, and inspect approach.

import puppeteer from 'puppeteer';

const url = process.env.TARGET_URL;
if (!url) throw new Error('Set TARGET_URL to the page URL');

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

  await page.evaluate(async () => {
    const pause = ms => new Promise(resolve => setTimeout(resolve, ms));
    let oldHeight = 0;
    let stable = 0;
    for (let pass = 0; pass < 30 && stable < 2; pass++) {
      const height = document.documentElement.scrollHeight;
      const step = Math.max(300, Math.floor(innerHeight * 0.8));
      for (let y = 0; y < height; y += step) {
        scrollTo(0, y);
        await pause(300);
      }
      await pause(500);
      const nextHeight = document.documentElement.scrollHeight;
      stable = nextHeight === oldHeight ? stable + 1 : 0;
      oldHeight = nextHeight;
    }
    scrollTo(0, 0);
    await pause(300);
    await Promise.all([...document.images].map(img => img.complete ? Promise.resolve() :
      new Promise(resolve => {
        img.addEventListener('load', resolve, { once: true });
        img.addEventListener('error', resolve, { once: true });
      })
    ));
  });

  const images = await page.$$eval('img', nodes => nodes.map(img => ({
    src: img.currentSrc || img.src,
    complete: img.complete,
    naturalWidth: img.naturalWidth
  })));
  console.warn('Images needing review:', images.filter(img => !img.complete || img.naturalWidth === 0));
  await page.screenshot({ path: 'documentation.png', fullPage: true });
} finally {
  await browser.close();
}

5. Check image and network state

The browser’s HTMLImageElement.complete property can help identify whether an image finished loading. Pair it with naturalWidth: a complete image with a zero natural width may have failed. For CSS background images, video posters, canvas content, or custom components, inspecting document.images is not enough; check the relevant element and network requests.

Use Chrome DevTools’ Network panel to inspect requests around the time each section appears. Its network screenshot capture can associate captured loading frames with network activity. Check for blocked, failed, redirected, or delayed image requests, and determine whether the page changes an image’s source only after scrolling.

6. Edge cases to account for

  • Infinite scroll: scrolling may append content and increase document height. Repeat passes until the intended content is present; a bounded loop prevents endless capture.
  • Virtualized lists: the page may keep only visible items in the DOM. A full-page screenshot cannot include elements that the application has not rendered together. Capture sections as they appear or use a page-specific export mechanism.
  • Sticky headers and animations: scrolling can change fixed controls or trigger animation. Inspect the output and use a site-appropriate wait or state.
  • Responsive images: srcset and sizes can select different files at different viewport widths. Set the viewport before triggering loading and capture.
  • Images outside ordinary img elements: CSS backgrounds, canvases, and custom components need their own readiness checks.
  • Authentication and consent state: establish the intended browser state before scrolling; otherwise, the screenshot may document a different experience.
  • Very tall pages: full-page captures can consume substantial memory and produce unwieldy files. Capture meaningful sections when a single image is impractical.

7. Troubleshooting

Symptom Likely cause What to do
Images below the fold are blank They were deferred until near the viewport. Scroll through the page in steps, pause for loading, and check image state before capture.
The page load event fired, but images are missing The load event does not certify completion of offscreen lazy images. Trigger them by scrolling and inspect their completion state.
Network idle occurred, but content is missing Idle describes current network activity, not future interaction-triggered requests. Scroll, wait for relevant selectors or image state, and inspect requests.
An image remains missing after scrolling The request may fail or be blocked, or the site may use a custom deferred source. Inspect the Network panel, response, current source, and console; diagnose the underlying request or page behavior.
Capture hangs waiting for images An image may never fire a load or error event due to page behavior or a navigation interruption. Use a bounded timeout for readiness checks and report unresolved images instead of waiting indefinitely.
Lower content is absent on an infinite page Scrolling reveals more content and changes document height. Repeat scroll passes until the target content appears; set a practical pass or content limit.
Output has repeated or misplaced elements Sticky UI, animation, or layout shifts changed during scrolling. Wait for stable layout, adjust the site-specific capture approach, and inspect the result.
Wrong image variant appears Responsive image selection depends on viewport and device scale. Set the intended viewport and scale before scrolling and capturing.

8. Performance, reliability, and cost

Each scroll step adds waiting time; a smaller step gives the page more opportunities to reveal content but takes longer. Keep waits short enough for routine captures, then use explicit checks for the important images or sections. Do not compensate for a failed request by adding an arbitrarily long delay: inspect the request first.

For repeatable documentation, pin the viewport and browser state, use bounded retries or loops, record incomplete-image diagnostics, and inspect the resulting file. Browser automation uses compute and time on your own machine or runner; full-page rendering of a very long page also uses more memory. The exact resource cost depends on the page and environment.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Its full-page capture loads lazy images. Cookie banners are accepted and more than 60 known consent platforms, newsletter popups, and chat widgets are removed before the shot; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. AI agents can use its MCP tools to take screenshots. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots. See the ScreenshotNeo API documentation.

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,
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', new Uint8Array(await res.arrayBuffer()));

These examples save the returned image bytes; use your own documentation target URL in place of https://stripe.com. With Node.js, the final two lines use Bun’s file writer; in Node, replace them with import { writeFile } from 'node:fs/promises'; await writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));. Create a free account at ScreenshotNeo: 1,000 free screenshots each month, no card required.

9. Frequently asked questions

Does full-page mode scroll the page first?

Full-page mode describes the captured extent. Prepare the page and trigger deferred content before asking for the full-page image.

Is scrolling to the bottom once enough?

Not always. Infinite-scroll pages can grow as you scroll, and some implementations need repeated passes or a specific interaction.

Can I trust an image’s complete property by itself?

Use it with naturalWidth and, for important content, inspect the request and rendered output. A completed failed image is still not a usable image.

Should documentation include the capture conditions?

For records that need to be reproduced, keep the viewport, relevant browser state, and capture date alongside the screenshot.

Sources