How to Capture a Webpage After Lazy-Loaded Images Finish Loading
Scroll the page to trigger lazy loading, verify images loaded successfully, then capture a full-page screenshot with Playwright.
To capture a full-page screenshot that includes lazy-loaded images, first scroll through the page in viewport-sized increments so its lazy-loading code runs. Then wait for the relevant images to load successfully, optionally wait for them to decode, and take a full-page screenshot. In Playwright, use page.screenshot({ fullPage: true }) for the final capture.
A full-page screenshot can include content below the viewport without causing the page to experience the real scrolling that triggers that content. Navigation finishing—or waiting for the page’s load event—does not prove that below-the-fold images are ready.
1. Install Playwright and prepare a page
This runnable Node.js example uses Playwright’s Chromium browser. Install the package and browser, then save the script as capture.js. Pass the target URL as the first command-line argument.
npm install playwright
npx playwright install chromium
2. Scroll to trigger lazy loading, wait, and capture
// capture.js
const { chromium } = require('playwright');
async function capture(url, outputPath = 'page.png') {
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1,
});
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
// Real scrolling triggers viewport-based lazy loading and scroll handlers.
// Repeat if scrolling causes the document to grow, as with appended content.
await page.evaluate(async () => {
const pause = ms => new Promise(resolve => setTimeout(resolve, ms));
let previousHeight = -1;
let stablePasses = 0;
let passes = 0;
while (stablePasses < 2 && passes < 10) {
const height = document.documentElement.scrollHeight;
const step = Math.max(300, window.innerHeight * 0.8);
for (let y = 0; y < height; y += step) {
window.scrollTo(0, y);
await pause(150);
}
window.scrollTo(0, document.documentElement.scrollHeight);
await pause(300);
const nextHeight = document.documentElement.scrollHeight;
stablePasses = nextHeight === previousHeight ? stablePasses + 1 : 0;
previousHeight = nextHeight;
passes++;
}
});
// complete may also be true for a failed or empty image. Require image data.
await page.waitForFunction(
() => [...document.images].every(img => img.complete && img.naturalWidth > 0),
{ timeout: 15000 }
);
// Decode successful images when possible; a decode failure does not hide
// the earlier load check and can be handled by reviewing the page itself.
await page.evaluate(async () => {
await Promise.all(
[...document.images]
.filter(img => img.complete && img.naturalWidth > 0)
.map(img => img.decode().catch(() => undefined))
);
});
await page.screenshot({ path: outputPath, fullPage: true });
} finally {
await browser.close();
}
}
const url = process.argv[2];
if (!url) {
console.error('Usage: node capture.js https://example.com');
process.exit(1);
}
capture(url).catch(error => {
console.error(error);
process.exitCode = 1;
});
Run it with:
node capture.js https://example.com
The scroll step is 80% of the viewport height, leaving overlap so content near viewport boundaries has a chance to load. The pauses are starting points, not universal timing guarantees. Increase them for slow sites or replace them with page-specific readiness checks. The loop has a pass limit because infinite feeds may never stop growing.
3. Choose a reliable readiness check
HTML images
For ordinary <img> elements, img.complete alone is not enough: it can be true when the source is empty or the image failed. Pair it with img.naturalWidth > 0 to require available image data. img.decode() adds a signal that the image is decoded and ready to render, but its promise can reject if loading or decoding fails.
The generic check above can time out on deliberately broken, blocked, hidden, or decorative images. If the page has known content, a targeted condition is usually better:
await page.waitForFunction(() => {
const img = document.querySelector('.product-gallery img');
return img && img.complete && img.naturalWidth > 0;
}, { timeout: 15000 });
For a gallery, assert the expected number of successful images or wait for a loading placeholder to disappear. When a lazy-loaded image uses src or currentSrc only after it approaches the viewport, scroll first, then inspect that value.
Other content and page types
- Infinite feeds or appended rows: scroll repeatedly and stop when a known end marker appears, the expected item count is reached, or document height stays stable for a few passes. Do not wait for an infinite feed to become fully loaded.
- Nested scrolling: scroll the actual container, such as a gallery or results pane. Scrolling the window will not trigger content inside an independently scrolling element.
- Load more controls: click the page’s control and wait for a specific new item or count before continuing.
- CSS background images: they are not in
document.images. Wait for a page-specific state and check the relevant element’s computedbackgroundImageor application loading indicator. - Authentication or consent gates: establish the required page state before capture. A screenshot cannot show content the session is not allowed to access.
- Canvas or embedded content: use application-specific readiness signals; an HTML image check cannot confirm that a canvas or cross-origin frame has finished rendering.
- Animations and carousels: disable animations with test-specific CSS or wait for the desired frame so the capture is repeatable.
4. Configure the capture
Set the viewport before navigation if responsive layout affects which images load. The device scale factor controls pixel density. Playwright’s fullPage: true captures the full scrollable page; use a locator screenshot instead when you only need one element.
await page.screenshot({ path: 'page.png', fullPage: true });
await page.locator('.product-gallery').screenshot({ path: 'gallery.png' });
Pick one output format and path deliberately: PNG preserves detail without lossy compression but can be larger; JPEG is useful when a smaller photographic image matters more than lossless detail. If using an infinite page, capture a defined region or stop at a known endpoint because a full-page image of an unbounded document is not meaningful.
5. cURL and Python alternatives
Playwright is useful when you need to control scrolling and page-specific waits. A raw HTTP request does not run browser JavaScript and therefore cannot trigger browser viewport lazy loading. These examples call a ScreenshotNeo capture endpoint, which handles browser capture for you; see the ScreenshotNeo API documentation for its options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as f:
f.write(r.content)
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Images are missing in the lower part of the screenshot | Full-page capture did not trigger real scrolling, or the scroll pause was too short. | Scroll in increments before capture; increase the pause or wait for a specific image or loading state. |
| The image wait times out | A broken or blocked image, an empty source, or a page-specific image outside the generic check. | Log images where naturalWidth is zero, exclude known optional images, and wait on the content the task actually requires. |
| Document height keeps increasing | The page appends content on scroll or is an infinite feed. | Use a pass limit, expected item count, or end marker; capture a bounded section if there is no finite endpoint. |
| Scrolling the page does not load gallery items | The gallery has its own scroll container. | Scroll that container and wait for its item count or a target element. |
networkidle never occurs or still misses images |
Analytics or polling keep requests active, or lazy requests start only after scrolling. | Do not treat network quiescence as proof of visual readiness. Use scroll actions and explicit assertions. |
| Screenshot differs between runs | Animations, rotating content, responsive layout, or network timing changes page state. | Fix viewport and session state, disable animations where appropriate, and wait for a stable page-specific condition. |
| Navigation fails or page is blank | Timeout, network failure, bot check, authentication, or a site error. | Check the URL and browser logs, increase navigation timeout only when justified, and provide the required session state. A longer timeout cannot repair a blocked page. |
7. Performance, reliability, and cost
Scrolling and waiting add time in proportion to page height, number of passes, and pause duration. Large images and very long pages also increase memory use and output size. Keep the viewport and scroll step appropriate to the page, use a targeted readiness assertion when possible, cap repeated passes, and set timeouts so a failed resource does not stall a capture indefinitely.
Reliability depends on the page’s loading strategy. Native lazy loading defers fetching images until they are expected near the viewport; application code may use intersection observers, nested containers, or explicit controls. Some pages also place visuals in CSS backgrounds, canvas, or frames, which the generic image check does not cover. Keep a record of timed-out images or failed page conditions so the caller can distinguish an incomplete capture from a successful one.
Self-hosted Playwright has no per-capture API charge described here, but it requires browser installation and compute, and you maintain the automation. A screenshot API trades that setup for a per-plan service cost; compare the plan allowance and the capture behavior you need. No universal timing or cost benchmark applies across sites.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. Its full-page capture loads lazy images. One GET request returns an image or PDF; the request below saves a WebP screenshot. See the API docs for configuration options.
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
Cookie banners, 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 for 1,000 free screenshots a month, with no card.
FAQ
Does fullPage: true trigger lazy loading by itself?
Do not rely on it to trigger scroll-dependent page code. Scroll the page before capture, then verify the required content is ready.
Should I wait for load or networkidle?
Neither is a universal image-readiness signal. Use explicit image or page-state checks; network activity can continue after useful content is ready or remain quiet before scroll-triggered loading starts.
Why check both complete and naturalWidth?
complete can be true for failed or empty images. A positive natural width indicates intrinsic image data is available.
Will this handle every lazy-loading implementation?
No generic script can cover every site. Nested scrollers, infinite feeds, CSS backgrounds, authentication, and app-specific loading need tailored actions and readiness checks.
Sources
- Playwright screenshot documentation describes full-page and element screenshots.
- Playwright Page API documents navigation waiting states and cautions against using
networkidleas a general readiness test. - Playwright issue 40941 discusses scrolling-dependent content and a proposed pre-capture behavior; it is an issue proposal, not a released option.
- MDN: HTMLImageElement loading and MDN: img element explain lazy loading.
- MDN: HTMLImageElement complete, naturalWidth, and decode() describe image readiness and failure cases.


