ScreenshotNeo

BlogHow-to

Puppeteer screenshot of a long page: avoid missing content after lazy loading

A full-page screenshot does not trigger every lazy load. Scroll the page, wait for meaningful content, then capture and verify the result.

By the ScreenshotNeo team4 October 20269 min read

page.screenshot({ fullPage: true }) captures the page’s full dimensions, but it does not guarantee that every lazy-loaded image or section has loaded. Scroll through the page first so viewport-based loading can run, wait for a page-specific sign of readiness when possible, then capture and inspect the result.

This guide shows a runnable Puppeteer workflow, explains what each wait does and does not guarantee, and covers nested scrollers, infinite feeds, timeouts, and validation.

1. Why full-page screenshots can miss content

Many pages defer work until content approaches the viewport. That can include images, embedded media, and entire content sections. A full-page screenshot requests a tall capture; it is not the same as scrolling through the page in a way that triggers every site’s loading behavior.

Navigation completion is not proof of visual completeness either. Puppeteer’s networkidle0 and networkidle2 lifecycle events describe network connections staying below their respective thresholds for 500 ms. A quiet network does not establish that the application rendered all deferred content. A page may also keep connections open, making network-idle waits time out. See the [Puppeteer lifecycle event reference](https://pptr.dev/api/puppeteer.puppeteerlifecycleevent) and [network idle method](https://pptr.dev/api/puppeteer.page.waitfornetworkidle).

2. Install Puppeteer and run a basic capture

In a new project, install Puppeteer:

npm install puppeteer

Save the following as screenshot-long-page.mjs, then run node screenshot-long-page.mjs https://example.com. It scrolls the document in viewport-sized increments, gives each position a short opportunity to trigger viewport-based loading, makes a final pass to the bottom, and captures a full-page PNG.

import puppeteer from 'puppeteer';

const url = process.argv[2];
if (!url) {
  throw new Error('Usage: node screenshot-long-page.mjs https://example.com');
}

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

  await page.evaluate(async () => {
    const step = Math.max(300, Math.floor(window.innerHeight * 0.8));
    let previousHeight = 0;
    let stableHeightPasses = 0;

    // A bounded loop avoids running forever on pages that keep growing.
    for (let pass = 0; pass < 80; pass++) {
      const height = document.documentElement.scrollHeight;
      const maxY = Math.max(0, height - window.innerHeight);
      for (let y = 0; y <= maxY; y += step) {
        window.scrollTo(0, y);
        await new Promise(resolve => setTimeout(resolve, 150));
      }
      window.scrollTo(0, maxY);
      await new Promise(resolve => setTimeout(resolve, 300));

      const newHeight = document.documentElement.scrollHeight;
      stableHeightPasses = newHeight === previousHeight ? stableHeightPasses + 1 : 0;
      previousHeight = newHeight;
      if (stableHeightPasses >= 2) break;
    }
    window.scrollTo(0, 0);
  });

  // Optional settling wait. It can time out on pages with persistent traffic.
  await page.waitForNetworkIdle({ idleTime: 500, timeout: 5000 }).catch(() => {});
  await page.screenshot({ path: 'page.png', fullPage: true });
  console.log('Saved page.png');
} finally {
  await browser.close();
}

The delays and loop limit are starting values, not universal settings. A fixed 150 ms pause can be too short on a slow page or unnecessarily long on a fast one. Prefer a site-specific readiness condition and a known end marker when available. The document-height check helps detect appended content, but it cannot prove that every image or application component finished rendering.

Puppeteer’s official [screenshots guide](https://pptr.dev/guides/screenshots) demonstrates navigation followed by a screenshot, while the [ScreenshotOptions reference](https://pptr.dev/api/puppeteer.screenshotoptions) documents fullPage, which is false by default. The workflow above adds scrolling because the target page may use scroll-triggered loading.

3. Choose a readiness signal that matches the page

Use the strongest practical signal the page exposes. A fixed delay is simple but weak. A visible section, disappearance of a loading indicator, expected item count, or end marker provides evidence about the content you need.

Wait for a known element

await page.locator('[data-testid="article-end"]').wait();

Use a selector that means the content of interest has arrived, rather than a generic page wrapper that appears immediately. Puppeteer locators also support waits for visibility and function-based conditions; see the [page interactions guide](https://pptr.dev/guides/page-interactions).

Wait for a target image

await page.waitForFunction(() => {
  const img = document.querySelector('img[data-critical="true"]');
  return img && img.complete && img.naturalWidth > 0;
}, { timeout: 10000 });

This checks one image only. If the page has several important images, check a list of selectors or verify the expected image count. An image can be complete but broken, which is why checking naturalWidth is useful.

Wait for an expected item count

await page.waitForFunction(expected => {
  return document.querySelectorAll('.feed-item').length >= expected;
}, { timeout: 10000 }, 40);

Replace .feed-item and 40 with page-specific values. This is appropriate only when the expected count is known and the site inserts those items into the document.

Use network idle as a secondary signal

await page.waitForNetworkIdle({ idleTime: 500, timeout: 10000 });

idleTime controls how long network activity must remain quiet; a timeout bounds how long the call waits. Treat this as a settling hint, not a content-completeness check. The page’s application state and your required content remain the deciding signals.

4. Handle nested scroll containers

Some pages put the feed inside a panel with its own scrollbar. Scrolling window will not expose content in that panel. Scroll the relevant element itself. Puppeteer locators provide scroll(), which uses mouse-wheel events, and support scrolling an element with offsets.

const feed = page.locator('.virtualized-feed');
await feed.scroll({ scrollTop: 1200, scrollLeft: 0 });

For a long container, advance it repeatedly and check a content condition or the container’s scroll height and position. Do not assume the page’s total document height reflects the contents of a nested scroller. If the target is an element screenshot, ElementHandle.screenshot() scrolls that known element into view before capture; it does not load every offscreen item in a long page. See the [ElementHandle screenshot reference](https://pptr.dev/api/puppeteer.elementhandle.screenshot).

5. Infinite scroll and virtualized pages

Infinite feeds may keep adding items as you scroll, so “scroll until the height stops changing” can run indefinitely or stop too early. Set a clear stopping rule:

  • Stop when a visible end marker appears.
  • Stop when the expected number of items is present.
  • Stop after a fixed number of scroll rounds if the page has no reliable end condition, and report that the capture is bounded.

Virtualized lists often remove offscreen items from the DOM and reuse a small set of elements. A full-page screenshot may therefore contain only the currently rendered window or may not represent the entire logical list. If the goal is a record of all entries, capture slices while scrolling and combine them in a workflow designed for that output, or use the site’s data/export interface where permitted. If the requirement is one continuous image, confirm the page actually renders the full content in the DOM before relying on fullPage.

6. Screenshot options and capture choices

Need Puppeteer approach Important detail
Entire document page.screenshot({ path: 'page.png', fullPage: true }) Capture size follows the document dimensions; scroll and verify deferred content first.
Current viewport page.screenshot({ path: 'viewport.png' }) Default is not full-page.
One known element elementHandle.screenshot({ path: 'element.png' }) Brings that element into view; does not trigger every other lazy item.
Image bytes in memory const image = await page.screenshot() Omit path when you want the returned image data instead of saving directly.

Set viewport dimensions before navigation if responsive layout affects content. The full-page flag controls capture extent; it does not configure readiness, scroll behavior, device emulation, or site-specific loading.

7. Validate the captured output

  1. Open the image and confirm the expected final section is present.
  2. Check critical images for blank placeholders or broken-image markers.
  3. Compare visible item counts or section headings with the expected content.
  4. For automated capture, record the page URL, viewport, readiness condition, and whether any timeout was tolerated.

Successful navigation and a successful screenshot call only show that Puppeteer produced an image. They do not confirm that the page was complete. Validation should check the content the capture was intended to preserve.

8. Troubleshooting

Symptom Likely cause Fix
Bottom sections or images are missing Only fullPage was set; the page never scrolled through deferred content. Scroll in increments before capture, then wait for a content-specific signal.
Network-idle wait times out Persistent connections, polling, or ongoing requests keep activity above the threshold. Use a page-specific readiness check; make network idle optional or give it a bounded timeout.
Scroll loop never ends Infinite scroll keeps increasing page height. Use an item count, end marker, or maximum number of rounds.
Document scroll does nothing The content is in a nested scrolling element. Scroll the container with a locator and verify its own content and position.
Capture is very tall or fails The document is exceptionally long, making the resulting image large or expensive to handle. Set a deliberate maximum scope, capture sections separately, or capture a specific element.
Images remain blank despite scrolling The site may need more time, a different scroll trigger, or a successful image request; some content may be blocked. Check the image’s complete and naturalWidth, inspect page errors, and wait for the relevant condition.
Output shows only part of a feed The page virtualizes items or appends them only on interaction. Scroll with an explicit stopping rule, verify what remains in the DOM, and capture slices if needed.

9. Performance, reliability, and cost

Each scroll step and readiness wait adds time. A large number of small steps can be slow; very large steps can skip the viewport conditions a site relies on. Start around 80% of the viewport height, then tune against the target page and verify the final content. Keep navigation, selector, idle, and overall job timeouts bounded so a broken or endless page cannot hold a worker indefinitely.

Full-page images can consume substantial memory and storage when the document is tall or the viewport is wide. Limit the capture to the required page or element where possible. For repeated jobs, reuse a browser process where appropriate, but isolate pages and close them reliably after each capture. Ensure the browser is closed in a finally block as in the example.

Self-hosted Puppeteer has no per-screenshot ScreenshotNeo charge; operational costs come from the machine, browser runtime, storage, and engineering time. If using a hosted browser environment, check that provider’s current pricing and limits directly. Do not equate a zero exit status with a correct image: failures in page loading and readiness logic need monitoring and output validation.

10. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Its full-page option loads lazy images before the capture. Call the API with one GET request:

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

See the ScreenshotNeo API documentation for the request options. The same endpoint can return PNG, JPEG, WebP, or PDF; it also supports selecting one element with a CSS selector. For this title’s workflow, the relevant benefit is that its full-page capture loads lazy images.

With ScreenshotNeo, cookie and consent 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 screenshots.

Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.

FAQ

Does fullPage: true scroll the page first?

It requests a full-page capture. Scroll beforehand when the site defers content until it approaches the viewport.

Is networkidle2 enough?

Not by itself. It is a network lifecycle condition, not proof that the application rendered every section or image you need.

How long should I wait after each scroll?

There is no universal delay. Use a page-specific condition when possible; otherwise, tune a bounded delay for the target and inspect the output.

Can Puppeteer capture an infinite page completely?

Only if you define what complete means. Use an end marker or item count, or stop after an explicit limit; an endless feed has no natural final height.