ScreenshotNeo

BlogHow-to

How to capture a full webpage without cutting off lazy-loaded images

Capture the entire page and give lazy-loaded images a chance to appear. Use this manual workflow or automate scrolling, waiting, and verification with Playwright.

By the ScreenshotNeo team4 October 20266 min read

To capture a full webpage without missing lazy-loaded images, scroll through the page first, pause while below-the-fold content loads, then take the full-page screenshot and inspect the lower sections. A full-page option captures the page’s scrollable extent; it does not guarantee that every image or dynamically loaded section has finished appearing. The browser’s load event alone is not enough to establish that lazy images are ready.

Why full-page screenshots can miss images

Full-page capture and resource loading are separate steps. A screenshot tool can include the document’s full scrollable height while off-screen images remain deferred until they approach the viewport. Scrolling gives viewport-based lazy-loading mechanisms a chance to run. Dynamic pages may need additional time or site-specific checks; there is no universal delay that guarantees every page is ready.

MDN notes that the load event fires when eagerly loaded content has loaded. Lazy images may still be pending. An image’s complete property is one useful loading-state check, but it does not prove the image looks correct or account for all content that scripts may add later.

Manual workflow

  1. Open the page and let its initial content render.
  2. Scroll down gradually through the whole document. Pause when images or sections appear to be loading.
  3. If the site appends content as you scroll, continue until you reach the intended end. Decide what counts as complete for an infinite-scrolling page.
  4. Return to the top if your capture tool works best from there, then take its full-page screenshot.
  5. Inspect the lower sections of the screenshot. If images are missing, scroll those sections into view, wait for the page to update, and capture again.

A fixed wait can be a useful minimum pause, but it is not proof that a particular site’s images or dynamically inserted content have finished loading. Check the result.

Automate it with Playwright

Playwright’s fullPage: true option captures the full scrollable page. The example below scrolls through the document in viewport-sized steps to trigger viewport-based loading, waits briefly between steps, checks image completion state, and then captures. The pause is a configurable heuristic, not a universal guarantee; adjust it for the site and verify the output.

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: 1365, height: 900 } });

try {
  await page.goto(targetUrl, { waitUntil: 'load', timeout: 60000 });

  // Scroll down in viewport-sized steps so off-screen content can approach the viewport.
  await page.evaluate(async () => {
    const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
    const step = Math.max(300, Math.floor(window.innerHeight * 0.8));
    let previousHeight = 0;
    let stableHeightPasses = 0;

    while (stableHeightPasses < 3) {
      const height = document.documentElement.scrollHeight;
      for (let y = 0; y < height; y += step) {
        window.scrollTo(0, y);
        await pause(250);
      }
      await pause(500);

      const newHeight = document.documentElement.scrollHeight;
      if (newHeight === previousHeight) stableHeightPasses += 1;
      else stableHeightPasses = 0;
      previousHeight = newHeight;
    }

    window.scrollTo(0, 0);
  });

  // Wait for images currently in the DOM to settle, up to a bounded timeout.
  const imageStatus = await page.evaluate(async () => {
    const images = Array.from(document.images);
    await Promise.all(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 });
      });
    }));
    return images.map((img) => ({
      src: img.currentSrc || img.src,
      complete: img.complete,
      naturalWidth: img.naturalWidth
    }));
  });

  const incomplete = imageStatus.filter((img) => !img.complete || img.naturalWidth === 0);
  if (incomplete.length) {
    console.warn('Images still incomplete or failed:', incomplete);
  }

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

Save this as capture.mjs, install Playwright with npm install playwright, and run node capture.mjs https://example.com. The image check waits for load or error events on images present in the DOM; broken images can become complete after an error, so the naturalWidth check helps flag those. For sites that keep adding images or content, define a site-specific completion condition instead of treating a stable page height as proof of completion.

Playwright API reference: Page screenshot options. Browser lazy-loading behavior and the limits of the load event: MDN: Lazy loading.

Options and edge cases

  • Infinite scrolling: Scrolling may append more content and increase page height. The example stops after several passes with unchanged height, but some sites load content after a longer delay or require a specific interaction. Set an explicit end condition, such as a known final item or end marker, when available.
  • Slow or failed images: A timeout does not make a failed image appear. Check the image’s loading state, its network request, and whether the page itself shows the image. Retry only when a transient failure is plausible.
  • Images inserted by scripts: A list of document.images only reflects images present when that list is collected. If scripts insert more images afterward, repeat the check after the page-specific content is ready.
  • Very tall pages: Full-page screenshots can produce large files and take longer to capture. Playwright supports screenshot scale and clipping options; choose output settings according to readability and file-size needs. A full-page capture may also be unsuitable when the page keeps growing without a defined end.
  • Viewport-dependent layouts: Responsive pages may use different images or layout at different viewport sizes. Set the viewport to the dimensions you intend to document before scrolling and capture.

Troubleshooting

Symptom Likely cause What to do
Lower-page images are blank They were still deferred when capture began, or the image request failed. Scroll the affected area into view, wait for it to update, then check the image’s complete and naturalWidth values. Inspect the page and request if it remains absent.
Only the initial page content appears The page appends content on scroll, or the capture happened before that content was added. Scroll through the document and use a site-specific end condition. Confirm the expected final section exists before capturing.
The script waits indefinitely for images An image request may never fire a load or error event in the expected way. Use a bounded timeout around image waiting, report unresolved images, and still inspect the result. Do not let one resource block the entire job forever.
Screenshot height changes between runs Dynamic content, late image sizing, ads, or infinite scrolling changes the document height. Wait for the page-specific content to settle, repeat the scroll-and-height check, and define which content should be included.
The screenshot is too large The document is very tall or the selected screenshot scale is high. Use an appropriate screenshot scale or capture a deliberate clip. Confirm text and images remain readable at the resulting size.

Performance, reliability, and cost

Scrolling the entire page and waiting for images adds work proportional to page length and resource behavior. A shorter pause can speed up capture but may miss delayed content; a longer pause can reduce that risk without proving completion. Prefer a meaningful site-specific signal when available, and put upper bounds on navigation and resource waits so one stalled page does not hold a job forever.

For repeatable automation, record the target URL, viewport, capture time, and any incomplete image warnings alongside the output. Re-run or flag captures where expected images are absent. A successful screenshot call only establishes that an image was produced; it does not establish that every lazy resource rendered correctly. Costs depend on the browser infrastructure and capture service you choose; this workflow itself does not imply a universal price or runtime.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Its full-page capture loads lazy images. One GET request returns an image or PDF; see the 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)
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 removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed, and response headers say the page verdict and whether the shot was billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.

Sign up for 1,000 free screenshots a month, no card required.

FAQ

Does fullPage: true trigger lazy loading by itself?

It captures the full scrollable extent. Scroll through the page first so viewport-based lazy loading has a chance to run, then inspect the output.

Is the load event enough?

No. It covers eagerly loaded content; lazy-loaded images may still be pending.

Does img.complete mean an image displayed correctly?

No. It is a loading-state check. Also inspect naturalWidth and the screenshot, since completion alone does not prove a visible, correct image.

How long should I wait?

There is no universal wait duration. Use a site-specific ready condition where possible, and verify the captured result.