How to Fix Screenshot API Captures of Lazy-Loaded Images
Below-the-fold images can be missing because full-page capture may not trigger scrolling. Scroll the page, wait for the images you need, then capture.
If images below the fold are missing from a full-page screenshot, trigger the page’s lazy-loading behavior before you capture. Scroll through the page in controlled steps, wait for the images or content you need to finish loading, and then take the full-page screenshot. A full-page capture does not necessarily scroll the live viewport, so it may not trigger loading that depends on viewport intersections or scroll events.
Do not treat a successful navigation event or network-idle state as proof that every image is ready. Lazy image requests may not start until scrolling brings them near the viewport. The exact scroll distance and wait depend on the page and renderer, so validate the sequence on representative pages.
Why full-page screenshots miss lazy images
Full-page capture and scrolling the page are different operations. Playwright describes fullPage: true as capturing the full scrollable page. A Playwright issue discusses how off-screen content can be rendered for a full-page screenshot without moving the visual viewport. If a page relies on scrolling or IntersectionObserver callbacks to load images, iframes, or other content, a direct full-page capture may happen before those callbacks run.
This explains a common failure mode; it is not a guarantee that every browser or screenshot service behaves the same way. Check the behavior of the renderer you actually use. See the [Playwright screenshot documentation](https://playwright.dev/docs/screenshots) and [the issue discussing scroll-triggered content](https://github.com/microsoft/playwright/issues/40941).
A reliable capture sequence
- Navigate and wait for an initial state. Choose a navigation condition appropriate to the page. This confirms progress through navigation, not that deferred images are loaded.
- Scroll in steps from top to bottom. Allow the page’s lazy-loading callbacks to run as successive areas approach the viewport. On long pages, a single jump to the bottom can skip intermediate intersections.
- Wait for the content your screenshot needs. In browser automation, check the relevant image or content condition after the scroll pass. A fixed delay can be a fallback, but elapsed time alone is not proof that a particular image loaded.
- Capture the full page. Take the screenshot after the readiness condition is satisfied.
- Validate on representative pages. Include long pages, pages that append content dynamically, and pages with images far below the fold.
There is no universally correct scroll increment or delay. Tune them against the target pages and the browser or API renderer in use.
Playwright example: scroll, check images, then capture
This runnable Node.js example uses Playwright. It scrolls through the page in viewport-sized steps, waits briefly between steps to give scroll-triggered loading a chance to run, waits for image elements to finish loading or fail, and writes a full-page screenshot. Replace the URL and output path for your use case.
import { chromium } from 'playwright';
const url = 'https://example.com';
const browser = await chromium.launch();
const page = await browser.newPage();
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
// Re-read page height while scrolling because pages may add content dynamically.
let previousHeight = 0;
let stableHeightPasses = 0;
const maxPasses = 100;
for (let pass = 0; pass < maxPasses; pass += 1) {
const metrics = await page.evaluate(() => ({
y: window.scrollY,
viewport: window.innerHeight,
height: document.documentElement.scrollHeight,
}));
const nextY = Math.min(metrics.y + Math.max(1, metrics.viewport * 0.8), metrics.height);
await page.evaluate((y) => window.scrollTo(0, y), nextY);
await page.waitForTimeout(250);
const height = await page.evaluate(() => document.documentElement.scrollHeight);
if (height === previousHeight && nextY >= height - window.innerHeight) {
stableHeightPasses += 1;
if (stableHeightPasses >= 2) break;
} else {
stableHeightPasses = 0;
}
previousHeight = height;
}
// Wait for images currently in the document to report completion.
// Broken images also complete; inspect naturalWidth if successful decoding is required.
await page.waitForFunction(() =>
Array.from(document.images).every((img) => img.complete)
, null, { timeout: 30_000 });
// Optional stricter check: fail if any image did not load successfully.
const broken = await page.evaluate(() =>
Array.from(document.images)
.filter((img) => img.complete && img.naturalWidth === 0)
.map((img) => img.currentSrc || img.src)
);
if (broken.length) {
throw new Error(`Images failed to load: ${broken.join(', ')}`);
}
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
Install Playwright in your project with npm install playwright. The loop uses overlapping steps instead of one large jump so intermediate viewport intersections are more likely to run. The short per-step wait is a starting point, not a universal value. The height-stability check is bounded to avoid an endless loop on pages that continually append content. If your target uses a known selector for the images or a “load more” control, add an explicit condition for it. Review [Playwright’s Page API](https://playwright.dev/docs/api/class-page) for navigation and wait behavior.
Adapting the readiness check
- Wait for specific images: Select the images required in the output and check that they are complete and have a nonzero
naturalWidth. If the site replaces image URLs or creates elements dynamically, identify them after the scroll pass. - Wait for a content marker: If the page exposes a selector that appears when a section is ready, wait for that selector after scrolling it into view.
- Handle images added later: A single check of
document.imagescan miss images appended after the check. Wait for the page’s known completion marker or poll until the required selectors and image states are stable. - Be aware of failed images:
img.completeis true both for successfully loaded images and for completed failures. ChecknaturalWidthwhen a broken image should fail the capture workflow.
Hosted screenshot APIs and lazy-load settings
Some hosted APIs provide a setting that scrolls through a page before capture. For example, ScreenshotAPI documents lazy_load, scroll_delay, and a separate post-load delay; its documentation says lazy_load defaults to false. These are provider-specific controls, not standard parameters across screenshot APIs. Consult the provider’s current documentation and check its timeout limits before relying on exact settings. See [ScreenshotAPI’s lazy-loading and delay documentation](https://www.screenshotapi.net/docs/renderScreenshot/lazy-loading-and-delay).
When evaluating an API for this case, check whether it:
- Actually scrolls the page before full-page capture.
- Lets you configure the delay between scroll steps and a wait after page load.
- Lets you express or verify a readiness condition for required content.
- Supports full-page output and gives enough time for long or dynamic pages.
- Behaves as expected on your pages, browser engine, and content patterns.
For direct browser automation, use the scroll-and-readiness sequence above. For a hosted service, verify the equivalent behavior in that service’s own documentation rather than assuming a parameter from another provider will work.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. Its full-page capture loads lazy images. A single GET request returns an image or PDF; see the ScreenshotNeo API documentation for options and response details.
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(`ScreenshotNeo request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server lets AI agents use screenshot tools. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Learn about ScreenshotNeo, then sign up for 1,000 free screenshots a month with no card.
Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| Images below the fold are blank, but images near the top appear. | The capture happened without scroll-triggered loading. | Scroll in overlapping steps before capture, then check that required images completed. |
| The page reports network idle, but images are still missing. | The image requests had not started yet because the relevant areas were not exposed by scrolling. | Trigger scrolling first. Then wait for the actual image or content condition you need. Playwright documents navigation states and discourages using networkidle as a general readiness test; use an assertion for the relevant state. |
| Images in the middle of a long page are missing. | A single jump to the bottom may have skipped intermediate intersections. | Scroll through the page incrementally, allowing callbacks to run between steps. |
| The screenshot is taken before images finish loading. | The post-scroll wait is too short, or no readiness condition is checked. | Wait for the selected images to complete or for a page-specific ready marker. Tune timing against the target pages. |
| The script waits forever on an image check. | A required resource may never complete, or the page keeps adding content. | Set a finite timeout, report which resources remain pending, and use a bounded scroll loop. Decide whether failed optional images should block capture. |
| An image element says it is complete but appears broken. | Completion also includes failed loads. | Check naturalWidth or the relevant response, and handle broken images according to your capture requirements. |
| A hosted API ignores a lazy-loading parameter. | Parameters are provider-specific, or the option is disabled by default. | Check that provider’s current docs, exact parameter names, defaults, and plan or timeout constraints. |
Performance, reliability, and cost
Scrolling and waiting adds time because it gives deferred requests a chance to start and finish. Long pages can require many steps, and dynamic pages may grow during the pass. Use overlapping viewport steps, a bounded loop, and a relevant readiness condition to balance coverage against time. Avoid excessively short waits that produce incomplete output and excessively long fixed delays that hold up every capture regardless of page behavior.
For reliability, test the same workflow on short and long pages, lazy images near the middle and bottom, and content that loads after interaction or network responses. Record which required images were missing or broken so a capture failure can be distinguished from a site that intentionally serves no image. The research sources do not establish neutral pricing or service-quality comparisons among providers; estimate costs using the current provider terms and your expected capture volume.
FAQ
Does fullPage: true scroll the browser?
It requests a screenshot of the full scrollable page. It does not necessarily move the live viewport through the document or trigger every scroll-dependent callback. Verify the renderer and scroll explicitly when lazy content matters.
Should I always wait for networkidle?
No. It describes a network state, not whether a particular lazy image has been requested and decoded. Trigger the lazy-loading behavior first and check the content your output requires.
Can one fixed delay work for every page?
No. Page length, dynamic behavior, image size, and renderer performance vary. Prefer an explicit readiness condition, and use measured delays only as part of a bounded workflow.
Do I need to scroll to the very bottom?
For a complete full-page result, usually scroll through the whole document so all relevant areas can trigger loading. If only a specific region matters, scroll through that region and check its content before capturing the desired output.


