How to Capture All Lazy-Loaded Images on a Documentation Website
Scroll the page to trigger deferred images, check that they loaded, then capture the full page. Here’s a Playwright workflow, a DevTools method, and fixes for common misses.
To capture every lazy-loaded image in a documentation page, scroll through the page in steps, give each section time to load, verify the images you expect are present, and only then take a full-page screenshot. A full-page screenshot captures the page’s scrollable area, but it does not itself trigger every site’s lazy-loading behavior. A page’s ordinary load event also does not prove that below-the-fold images have loaded. MDN’s lazy-loading guide explains why deferred resources may remain unloaded after that event; Playwright’s screenshot guide documents full-page capture.
1. Decide what “capture all images” means
A screenshot and an image download are different outputs:
- One image of the rendered page: use a full-page screenshot after triggering and checking lazy content. This includes text, diagrams, layout, and images as pixels.
- The original image files: inspect and collect image resource URLs separately. A screenshot does not preserve the original files. You may need to inspect
imgelements, responsive image sources such assrcset, CSS backgrounds, and page scripts.
The steps below focus on a screenshot of the rendered documentation page. They work as a starting point for common scroll-triggered loading, but custom scripts can require additional interactions or site-specific readiness checks.
2. Capture a full-page screenshot with Playwright
Install Playwright and its Chromium browser, save the following as capture.mjs, and run it with a documentation URL. The script scrolls downward in viewport-sized increments, waits briefly at each position, returns to the top, checks standard image elements, and captures the full page.
npm install playwright
npx playwright install chromium
// capture.mjs
import { chromium } from 'playwright';
const url = process.argv[2];
if (!url) {
console.error('Usage: node capture.mjs https://example.com/docs');
process.exit(1);
}
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
// Visit each viewport-sized region to trigger common scroll-based loaders.
// The extra passes handle pages that grow as images or content appear.
let previousHeight = 0;
for (let pass = 0; pass < 4; pass++) {
const height = await page.evaluate(() => document.documentElement.scrollHeight);
const step = Math.max(300, Math.floor(window.innerHeight * 0.8));
for (let y = 0; y < height; y += step) {
await page.evaluate(y => window.scrollTo(0, y), y);
await page.waitForTimeout(250);
}
const newHeight = await page.evaluate(() => document.documentElement.scrollHeight);
if (newHeight === previousHeight && newHeight === height) break;
previousHeight = newHeight;
}
await page.evaluate(() => window.scrollTo(0, 0));
// This checks standard img elements, not CSS backgrounds or site-specific
// image components that have not placed a source in the DOM.
const imageStatus = await page.locator('img').evaluateAll(images =>
images.map(img => ({
src: img.currentSrc || img.src,
complete: img.complete,
naturalWidth: img.naturalWidth,
loaded: img.complete && img.naturalWidth > 0
}))
);
const missing = imageStatus.filter(img => !img.loaded);
console.log(`Found ${imageStatus.length} img elements; ${missing.length} not confirmed loaded.`);
if (missing.length) console.log(missing);
await page.screenshot({ path: 'docs-full.png', fullPage: true });
console.log('Saved docs-full.png');
} finally {
await browser.close();
}
Run it:
node capture.mjs https://example.com/docs
For standard HTML images, complete and a positive naturalWidth indicate that an image has loaded successfully. Review the reported missing images and inspect the screenshot; the check cannot see CSS background images or determine whether every site-specific illustration is expected. Playwright also supports element screenshots and returning screenshot bytes. See the Page API for the current screenshot options.
Why scroll in steps?
Many lazy-loading mechanisms defer image work until an image is near the viewport. Incremental scrolling gives those mechanisms a chance to run and lets the browser fetch the newly relevant resources. A single jump to the bottom can skip intermediate triggers in custom implementations. The interval above is a practical starting point, not a universal readiness guarantee; increase the pause or add a site-specific wait when images need more time.
3. Capture manually with Chrome DevTools
- Open the documentation page in Chrome and wait for the initial content to settle.
- Scroll through the page section by section. Pause where necessary for diagrams and other images to appear.
- Check that the expected images are visible. The page’s initial load event is not enough to establish that deferred images loaded.
- Open DevTools Device Mode, choose More options, then select Capture a full size screenshot.
Chrome documents the full-size screenshot action in its Device Mode guide. Device emulation can also help you capture a layout at a chosen viewport. Scroll-triggered images still need to be triggered and checked before you capture.
4. Tune the capture for the page
Readiness and waits
The example starts navigation with domcontentloaded, then waits while it scrolls. A fixed pause is simple but may be too short for a slow page or waste time on a fast one. If a specific image or section matters, wait for that content or a site-specific condition before capturing. Playwright discourages treating networkidle as a universal readiness signal; pages may keep network connections open, and network quiet does not prove that the images you need are present. Use the Page API to choose navigation and waiting behavior appropriate to the site.
Viewport and device behavior
Set the viewport before navigation, because responsive documentation sites may serve different layouts and image candidates at different widths. Use a mobile-sized viewport when the goal is a mobile capture. Device emulation can affect viewport dimensions and device scale; choose settings that match the layout you need to preserve.
Very long pages and fixed elements
A full-page screenshot is convenient for a page that fits into one manageable image. Very tall pages can create unwieldy output, and fixed or sticky navigation may render differently in a stitched full-page capture than during normal scrolling. Inspect the result. If it is hard to use, capture viewport-sized sections or screenshot a specific element with Playwright’s locator screenshot support.
Infinite scroll
Some documentation or reference sites append content as you scroll. The script makes a limited number of passes and stops when document height stabilizes; that is a guard against endless growth, not proof that an infinite page is complete. Decide what content range you need, scroll until that range is present, and use a site-specific stopping condition where possible.
5. Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| Images below the fold are blank | The page has not scrolled near them yet, or its loader needs more time or interaction. | Scroll closer in smaller steps, wait longer, and check whether the site requires expanding a section or another action. |
| The script ends after page load but misses images | The load event does not guarantee deferred images have loaded. |
Scroll through the document, inspect expected images, and wait on relevant content before capture. |
| Some images show as not loaded | The image request failed, the element has not received a source, or the site uses another rendering mechanism. | Check the URL printed by the script and the browser console/network panel. Inspect CSS backgrounds and custom image components separately. |
| Repeated or duplicated content appears in a full-page capture | Sticky or fixed elements may be rendered in a way that looks repeated in a tall image. | Inspect the output and consider viewport-sized captures or a targeted element screenshot. |
| The capture omits content on an infinite page | More content is appended only after scrolling, and the page has no fixed final height. | Continue scrolling until the required range is present and define a finite stopping condition for the capture. |
| The page or images require authentication | The browser context has no authorized session or access to the resources. | Use an authorized browser session and follow the site’s access terms. |
TimeoutError during navigation |
The page did not reach the selected navigation milestone within the timeout. | Check the URL and connectivity, raise the timeout if appropriate, and use a navigation condition suited to the site. Do not assume a longer timeout solves image-specific readiness. |
6. Performance, reliability, and cost
Scrolling in increments takes longer than issuing one full-page screenshot because the browser must visit regions and allow deferred requests to run. Keep the viewport and pauses only as large as your capture requirements need. For repeatable jobs, log the URL, image count, missing-image list, viewport, and output path so failures can be reviewed. A successful screenshot call confirms that a file was produced; it does not prove that every expected image was present.
Playwright is an open-source browser automation library; this workflow runs in your environment, so account for the machine time and browser setup involved. The dossier verifies no universal benchmark, runtime, or fixed cost for this task. If you need the original assets, downloading them is a separate workflow with its own network and access considerations.
Or skip the browser setup
For a one-call screenshot, ScreenshotNeo provides a website screenshot API. Its clean-shot flow accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each 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. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. Free includes 1,000 shots each month with no card; paid plans start at $5 for 3,000 shots. See the ScreenshotNeo API documentation for options and configuration.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/docs -o docs.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com/docs"},
timeout=90,
)
open("docs.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com/docs'
});
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('docs.webp', res);
These examples request a screenshot of the rendered page; they do not download each original image file. Sign up for 1,000 free screenshots a month with no card.
Frequently asked questions
Does a full-page screenshot automatically load every lazy image?
No. Trigger the page’s lazy-loading behavior and verify the content before taking the screenshot. Site-specific scripts can need additional actions.
Does loading="lazy" apply to every image on a page?
No. Pages can combine browser-native lazy loading with JavaScript loaders, responsive sources, CSS backgrounds, and other image components.
Can I use this method to save each original image?
No. It produces rendered pixels. Collect image URLs and download the assets separately if you need original files.
Should I wait for networkidle?
Not as a universal rule. A page-specific image or content check is more directly tied to whether the material you need is ready.


