ScreenshotNeo

BlogHow-to

How to Detect When a Lazy-Loaded Page Is Fully Rendered Before Capture

There is no universal “fully rendered” signal. Combine page-specific readiness checks, lazy-load triggers, and image validation before capture.

By the ScreenshotNeo team4 October 20269 min read

There is no universal browser signal that proves a lazy-loaded page is fully rendered. For a reliable capture, define “ready” as the page-specific content you need being present and visually settled. Use a navigation event as an initial checkpoint, wait for the expected content, trigger below-the-fold loading when needed, check the relevant images, and then capture.

A page’s load event covers dependent resources such as stylesheets, scripts, iframes, and images, but it cannot prove that later scroll-triggered content or application-specific updates have finished. Playwright recommends web assertions for assessing readiness, and discourages treating networkidle as a general readiness test. [Playwright navigation documentation] [Playwright Page API]

1. Define what “ready” means for this capture

Write down exactly what the screenshot must contain. Examples include a product heading and price, a gallery with all expected images, a results list in its final state, or a page-specific loading indicator disappearing. Prefer a semantic condition tied to the content over a generic timer or a quiet network.

Capture target Useful readiness condition
Viewport screenshot The visible page heading and required visible content are present; visible images have loaded.
Full-page screenshot Required page content is present after triggering below-the-fold loading; relevant images have usable dimensions.
Search or results page The final result container or expected result state appears, and any loading indicator is gone.
Application with explicit state A documented app-ready marker or page-specific state is true.

This is a deliberate definition for the capture, not proof that no more content could ever arrive. Pages can continue polling, animate, personalize content, or load more items on user action.

2. Playwright: wait for content, trigger lazy loading, validate images

The following Node.js example uses Playwright. It waits for the page’s initial load navigation checkpoint and a required heading, scrolls through the document to trigger common viewport-based lazy loaders, waits for images in the document to complete, and takes a full-page screenshot. Replace the URL and readiness selector with the target site’s actual content and success condition.

import { chromium } from 'playwright';

const url = 'https://example.com/catalog';
const readySelector = 'h1'; // Prefer a selector for the content this capture needs.
const outputPath = 'page.png';

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

  await page.goto(url, { waitUntil: 'load', timeout: 30_000 });
  await page.locator(readySelector).waitFor({ state: 'visible', timeout: 15_000 });

  // Scroll through the document to trigger common viewport-based lazy loaders.
  await page.evaluate(async () => {
    const step = Math.max(300, Math.floor(window.innerHeight * 0.8));
    for (let y = 0; y < document.documentElement.scrollHeight; y += step) {
      window.scrollTo(0, y);
      await new Promise(resolve => setTimeout(resolve, 100));
    }
    window.scrollTo(0, 0);
  });

  // Wait for document images to finish and verify images have usable dimensions.
  await page.waitForFunction(() => {
    const images = [...document.images];
    return images.every(img => img.complete && img.naturalWidth > 0);
  }, { timeout: 20_000 });

  // Optional: wait for a known site-specific loading indicator to disappear.
  // await page.locator('[data-testid="loading"]').waitFor({ state: 'hidden' });

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

Install and run it with npm install playwright and node capture.mjs (save the code as capture.mjs). The check above intentionally fails if any document image has no usable natural width. If the site includes optional or broken images that should not block capture, filter the set to the images that matter, for example by selector or a page-specific container.

Use an application-specific condition when possible

A visible heading is only an example. For a changing application, wait for the state that means the requested data is ready: a result count, a particular card, a completed status, or a loading marker hidden. With Playwright, use a locator wait or retrying assertion instead of an arbitrary delay.

// Example: wait for the relevant results and for a known spinner to disappear.
await page.locator('[data-testid="results"] article').first().waitFor({ state: 'visible' });
await page.locator('[data-testid="loading-spinner"]').waitFor({ state: 'hidden' });

Use selectors that reflect stable site structure. If the application exposes a reliable ready marker, prefer that over inferring readiness from an incidental element.

3. Trigger below-the-fold content only when the capture needs it

Browser-level image lazy loading can defer offscreen images until their position is known and they approach the viewport. Scrolling through relevant regions is a practical way to trigger it before a full-page capture. [Chrome guidance on browser-level image lazy loading]

  • Viewport capture: Usually check the visible content and its images; scrolling the entire document may add work without improving the screenshot.
  • Full-page capture: Scroll through the regions that must appear, then return to the desired starting position before capturing.
  • Custom JavaScript loader: Scrolling may not be sufficient. Follow the site’s documented trigger or wait for its specific content state.
  • Virtualized list: Offscreen rows may be removed from the DOM rather than simply waiting to load. A full-page screenshot may not include items never rendered; use the application’s export or pagination behavior if all items are required.

The scroll loop is a practical trigger, not a universal guarantee. It can activate more requests, alter sticky headers, or affect viewport-dependent layouts. Decide whether the target is the initial viewport or the entire document before using it.

4. Check the content types that matter

The sample validates regular <img> elements. A real page may need additional checks:

  • Images: Confirm the required image elements have completed and have a nonzero naturalWidth. A completed request can still be a broken image.
  • CSS background images: They are not in document.images. Check the page’s specific element or resource state if backgrounds are essential.
  • Canvas: Wait for the application’s render-complete state; image checks do not detect canvas drawing completion.
  • Video: If a frame is required, wait for the relevant media state and choose a deterministic frame strategy.
  • Iframes: The top-level page’s semantic ready condition may not mean embedded content is ready. Check the frame and its required content where access and origin rules permit.
  • Animations and transitions: Disable them with a test stylesheet or wait for a stable state if motion causes inconsistent captures.

5. Choosing navigation and waiting strategies

Strategy Good for Limit
load An initial checkpoint after dependent page resources load. Does not cover later scroll-triggered or application-specific work.
Semantic locator or assertion Verifying the content the capture actually needs. Requires a meaningful, stable selector or app state.
Scroll through relevant regions Triggering common viewport-based lazy loading for full-page capture. Adds time and may affect layout; custom loaders vary.
networkidle A framework option that can be useful on some quiet pages. Not proof of visual completeness; Playwright discourages it as a general readiness criterion.
Fixed delay A small settling buffer after meaningful conditions. Too short on slow pages, wasteful on fast pages, and never a universal readiness test.

Puppeteer’s screenshot guide shows networkidle2 as one navigation option. That is a usable pattern for some pages, not a guarantee that lazy content or application state is complete. [Puppeteer screenshot guide]

6. Capture a viewport instead of a full page

If only the visible area is required, use a viewport screenshot after the relevant visible content is ready. This avoids triggering unnecessary below-the-fold requests and reduces the amount of page behavior that can affect the result.

await page.screenshot({ path: 'viewport.png', fullPage: false });

For a full-page capture, retain fullPage: true and trigger content in the regions needed. Keep viewport dimensions consistent between runs because responsive layouts and lazy-loading thresholds can depend on viewport geometry.

7. Troubleshooting

Symptom Likely cause Fix
Images below the fold are missing The lazy loader had not been triggered before capture. Scroll through the relevant regions and wait for the required images to complete.
The image check times out A relevant image failed, an optional image is broken, or the page keeps adding images. Inspect which elements are incomplete; restrict validation to required images or handle known optional failures explicitly.
networkidle never arrives Polling, persistent connections, or other ongoing network activity. Use a semantic content condition and a bounded timeout; do not make network quiet the sole readiness rule.
Screenshot captures a spinner or skeleton The page shell loaded before the data or component finished rendering. Wait for the data-bearing element or for the site-specific loading marker to disappear.
Full-page screenshot is taller but still incomplete Some sites render only a viewport-sized region, virtualize content, or load on a custom trigger. Use the application’s own loading behavior; verify that the content exists in the DOM before capture.
Layout changes after scrolling back to the top Sticky elements, animations, or responsive behavior changed during the trigger pass. Use a page-specific strategy, disable motion for capture where appropriate, and restore the intended scroll position before capture.
Navigation times out although the page appears A resource or page lifecycle event did not finish within the chosen timeout. Inspect the page and choose a suitable navigation checkpoint, then rely on the required content condition with a bounded timeout.

8. Performance, reliability, and repeatability

  • Keep checks targeted. Waiting for every image on a large page can be expensive; validate the images included in the intended capture.
  • Use bounded waits. Set timeouts so a broken page does not hang a capture job indefinitely, and report which readiness condition failed.
  • Scroll in sensible increments. Smaller steps are more likely to trigger viewport observers; larger steps are faster but can skip some custom thresholds. There is no single interval that works for every implementation.
  • Make runs comparable. Keep browser, viewport, scroll method, readiness selectors, and relevant page settings consistent. Site content and environment changes can still alter rendering.
  • Use explicit failure handling. Treat a failed required-content check as a failed capture or a clearly marked incomplete result rather than silently saving a misleading screenshot.

For exact behavior, confirm the API and browser behavior against the framework version you install. The cited documentation is version-sensitive.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Its API can return an image or PDF from one GET request, with full-page capture and lazy images loaded available as options. 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}`);
  • Cookie and consent banners are accepted like a visitor, and 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 cost nothing. Response headers report the page verdict and whether the request was billed.
  • An MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs.
  • The Free plan includes 1,000 shots per month with no card. Paid plans start at $5 for 3,000 shots; yearly billing gives two months free. Every feature is on every plan.

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

FAQ

Does the browser’s load event mean a page is ready for a screenshot?

It is a useful initial checkpoint for dependent resources, but it does not establish that later lazy-loaded content or application work is complete.

Should I always wait for network idle?

No. Persistent activity can prevent it, and a quiet network does not establish that the exact content you need is present. Use a page-specific condition.

How do I know whether a lazy image loaded successfully?

For regular image elements, check that the image is complete and has a nonzero natural width, while accounting for optional or broken images the page may contain.

Is a fixed sleep ever useful?

It can be a short settling buffer after the meaningful checks pass. It should not replace those checks.

Sources