ScreenshotNeo

BlogHow-to

How to capture a full-page screenshot of a page with lazy-loaded images

Scroll the live page before capturing it: a full-page screenshot can include offscreen space without triggering the lazy-loaded images there.

By the ScreenshotNeo team4 October 20268 min read

To capture a full-page screenshot with lazy-loaded images, scroll through the live page in steps, wait for each newly visible section to load, then take the full-page capture. A full-page option expands the screenshot area; it does not necessarily scroll the page in a way that activates content tied to viewport visibility. Check the saved image afterward for missing images or sections.

This distinction matters because native lazy loading and many JavaScript loading patterns defer work until content approaches the viewport. Chrome describes loading="lazy" as deferring a resource until it reaches a calculated distance from the viewport. That distance can vary, so a fixed scroll interval or delay is not reliable for every site. Chrome Developers: browser-level image lazy loading.

Why a full-page capture can miss images

A screenshot API’s fullPage option captures the full scrollable page. Playwright documents that behavior, but it does not promise that the page’s own scroll-triggered code has run for every offscreen section. In practice, expanding the capture area and scrolling the actual viewport are separate operations. Playwright screenshot API.

Pages can defer images with HTML’s loading="lazy", JavaScript, or IntersectionObserver. They may also append new content near the bottom. A virtualized feed is a special case: it may only keep nearby rows in the document, so scrolling can replace earlier rows instead of building one tall page. CSS background images do not use the HTML image loading attribute.

Manual workflow in a browser

  1. Open the page and wait for its initial layout to settle.
  2. Scroll down in increments. Pause when needed so images and scroll-triggered content can load. Continue until you reach the end or the page stops growing.
  3. If the capture control depends on the starting position, return to the top.
  4. Capture the entire page using your browser’s full-page screenshot feature.
  5. Inspect the image from top to bottom. Look for blank image rectangles, missing sections, or repeated/missing list items.

Firefox offers a built-in full-page capture from its screenshot interface. Depending on your Firefox version and configuration, open the screenshot tool and choose Save full page. Mozilla documents this workflow in its Firefox screenshot guide. Scroll the page first when its content depends on viewport-triggered loading.

Automate the capture with Playwright

Install Playwright and its Chromium browser, save the following as screenshot.mjs, then run node screenshot.mjs https://example.com. The loop rechecks the page height because some sites append content as you approach the bottom. The 300 ms pause is only a starting value; increase it or wait for known page-specific content when necessary.

import { chromium } from 'playwright';

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

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

  // Scroll the live viewport so lazy and scroll-triggered content can load.
  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 stablePasses = 0;
    const maxPasses = 100;

    for (let pass = 0; pass < maxPasses && stablePasses < 3; pass++) {
      const height = document.documentElement.scrollHeight;
      for (let y = 0; y < height; y += step) {
        window.scrollTo(0, y);
        await pause(300);
      }
      await pause(300);
      const newHeight = document.documentElement.scrollHeight;
      stablePasses = newHeight === previousHeight ? stablePasses + 1 : 0;
      previousHeight = newHeight;
    }
    window.scrollTo(0, 0);
    await pause(300);
  });

  // Optional check: report images still not complete or without a natural size.
  const pending = await page.locator('img').evaluateAll(images =>
    images.filter(img => !img.complete || img.naturalWidth === 0)
      .map(img => img.currentSrc || img.src)
  );
  if (pending.length) console.warn('Images still incomplete:', pending);

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

Install and run:

npm install playwright
npx playwright install chromium
node screenshot.mjs https://example.com

The image check is diagnostic, not proof that every visible image has the right content. Some pages intentionally leave images unloaded, use CSS backgrounds, or use a virtualized list. If you know the expected image selector, wait for those specific images and verify their natural dimensions before capture.

Playwright options that affect the result

  • fullPage: true captures the full scrollable page; it does not replace the scroll pass.
  • path chooses the output file. The extension can determine the format.
  • type supports PNG, JPEG, or WebP. JPEG and WebP support a quality setting; PNG does not.
  • scale: 'css' produces one image pixel per CSS pixel; scale: 'device' uses device pixels and can create a larger image.
  • timeout sets the screenshot operation timeout. Navigation has its own timeout.
  • style can apply CSS while capturing, useful for hiding volatile elements, but should not hide content you need to inspect.

See the Playwright API reference for the full option list and current details.

Use Puppeteer with the same scroll-before-capture approach

Puppeteer also supports a full-page screenshot. Install it and Chromium, save this as screenshot.cjs, then run node screenshot.cjs https://example.com. As with Playwright, full-page capture does not guarantee a target page’s lazy content has rendered.

const puppeteer = require('puppeteer');

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

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

    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 stablePasses = 0;
      for (let pass = 0; pass < 100 && stablePasses < 3; pass++) {
        const height = document.documentElement.scrollHeight;
        for (let y = 0; y < height; y += step) {
          window.scrollTo(0, y);
          await pause(300);
        }
        await pause(300);
        const newHeight = document.documentElement.scrollHeight;
        stablePasses = newHeight === previousHeight ? stablePasses + 1 : 0;
        previousHeight = newHeight;
      }
      window.scrollTo(0, 0);
      await pause(300);
    });

    await page.screenshot({ path: 'page.png', fullPage: true });
  } finally {
    await browser.close();
  }
})();
npm install puppeteer
node screenshot.cjs https://example.com

Puppeteer documents Page.screenshot() and its screenshot options in the API reference. Choose a bounded navigation timeout and close the browser in a finally block so a failed page does not leave a process running.

Handle pages that keep loading or virtualize content

  • Growing page: repeat the scroll pass while checking document.documentElement.scrollHeight. Use a maximum number of passes, as in the examples, to avoid an endless loop on an infinite feed.
  • Known content: wait for a selector that identifies the expected section or image. A generic network-idle condition may never occur on pages with analytics, polling, or persistent connections.
  • Slow images: after scrolling, wait for specific image elements to become complete and have a nonzero naturalWidth. A timeout should turn a slow or broken resource into a diagnosable result instead of an unbounded wait.
  • Virtualized list: if offscreen rows disappear as new rows enter the viewport, one full-page image may not contain the whole feed. Capture separate viewport ranges or use the page’s export/data route if available.
  • Interaction-gated content: a click, consent choice, tab switch, or expand action may be needed before a section exists. Perform the intended interaction before the scroll pass.
  • Hidden images: an image hidden with display: none may not load while it remains hidden. Reveal the relevant page state before expecting it in the capture.

Performance, reliability, and output size

Each scroll step adds waiting time, so a slow page or a long document can take significantly longer than a viewport screenshot. Use a viewport-sized step with some overlap, wait only as long as the target page needs, and prefer known selectors or image-completion checks over an arbitrarily long delay. The example’s repeated height checks make room for appended content while its pass limit bounds runtime.

Very tall pages can produce large image files and may stress browser or image-decoder memory. Reduce screenshot scale when CSS-pixel output is sufficient, or capture in sections when the destination cannot handle one extremely tall image. Use PNG for lossless detail; JPEG or WebP with an appropriate quality can reduce file size when slight compression is acceptable. Verify the result visually because successful API completion only means a file was produced, not that every deferred resource loaded.

Troubleshooting

Symptom Likely cause Fix
Blank image areas below the fold The screenshot expanded the capture area without triggering viewport-based loading, or the wait was too short. Scroll in smaller steps, wait longer near the affected section, then capture again. Check whether the image request failed.
The page gets taller during capture Content is appended as the bottom approaches. Recheck document height and repeat the pass until it stabilizes; keep a maximum pass count for feeds that never end.
Only nearby list items appear The page may virtualize rows and remove items outside the viewport. Capture viewport segments or use a page-specific export. A single full-page screenshot may not represent all feed items.
An image stays blank while hidden Its element may be hidden, or its source is set only after a script or interaction. Reveal the relevant state, inspect src, srcset, and currentSrc, and trigger the site’s intended interaction.
Waiting for network idle hangs Analytics, polling, or persistent connections keep network activity open. Use domcontentloaded for navigation, then wait for specific content or image completion with a timeout.
Screenshot times out or browser exits early The page is unusually tall, resources are slow, or capture is attempted before the scroll pass finishes. Increase the relevant timeout, reduce scale, capture sections, and await each scroll and screenshot operation.
Timing changes between runs Lazy-loading distance and resource timing depend on the browser, network, and page code. Do not assume one hard-coded distance works universally. Verify expected images and inspect each output.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Its full-page capture loads lazy images. For a one-call image capture, replace the sample URL and API key below. See the ScreenshotNeo API documentation for request 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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An 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 free for 1,000 screenshots a month, no card required.

FAQ

Does fullPage: true scroll the page?

It requests a capture of the full scrollable page. Run a separate scroll pass first when the page loads content in response to viewport movement.

How long should I wait at each scroll position?

There is no universal delay. Start with a short pause, inspect the output, then increase the wait or wait for known selectors and images on pages that load slowly.

Will this work for CSS background images?

The HTML image loading attribute does not control CSS backgrounds. Scroll-triggered styles may still reveal them, but check the page’s CSS and output when a background is missing.

Can one screenshot include every item in an infinite feed?

Not necessarily. A feed may never end or may render only the rows near the viewport. Capture bounded sections or use a data/export method when you need every item.

Sources