Take a Full-Page Screenshot with Python and Selenium After Lazy-Loaded Images Finish
Scroll to trigger lazy images, wait for image loads, then capture the full document with Firefox WebDriver’s Python API.
To capture a full-page screenshot after native lazy-loaded images have had a chance to load, scroll through the page in viewport-sized steps, wait for image elements to finish, and use Firefox WebDriver’s save_full_page_screenshot(). Selenium’s ordinary screenshot method captures the current window; it should not be treated as a cross-browser full-document API. Firefox’s Python WebDriver reference documents both saving a full-page PNG and retrieving its bytes. Selenium Firefox WebDriver API and Selenium Remote WebDriver API.
What this workflow does
A page’s initial navigation completing does not prove its images or dynamic content are ready. Native lazy loading postpones fetching off-screen images until they approach the viewport, so the browser must scroll down before those images are requested. The window load event can occur before lazy images finish. MDN: Lazy loading and MDN: HTMLImageElement.complete.
The script below scrolls down in steps, waits until each image’s load attempt completes, and checks the page height again in case scrolling caused more content to appear. It then asks Firefox for a full-document PNG. A completed image request can still have failed, so the script reports images with a zero naturalWidth instead of pretending every image succeeded.
Install and run
Use Python 3, Selenium, Firefox, and a compatible Firefox driver. Selenium Manager can manage drivers in many standard Selenium setups; consult the current Selenium Manager documentation if driver startup fails. The Firefox full-page methods are browser-specific, so do not switch to another WebDriver and assume the same method exists.
python -m pip install selenium
Save this as full_page.py, then run python full_page.py https://example.com page.png.
import sys
import time
from selenium import webdriver
from selenium.webdriver.firefox.options import Options
def capture(url: str, output_path: str) -> None:
options = Options()
# Uncomment to run Firefox without a visible window:
# options.add_argument("-headless")
driver = webdriver.Firefox(options=options)
try:
driver.set_page_load_timeout(60)
driver.get(url)
# Navigation completion is only an initial readiness point. Scroll to
# bring viewport-triggered lazy images near the visible area.
previous_height = -1
stable_passes = 0
while stable_passes < 2:
height = driver.execute_script(
"return Math.max(document.body.scrollHeight, "
"document.documentElement.scrollHeight);"
)
viewport = driver.execute_script("return window.innerHeight") or 800
y = 0
while y < height:
driver.execute_script("window.scrollTo(0, arguments[0]);", y)
time.sleep(0.15) # brief opportunity for scroll-triggered work
y += viewport
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
# Wait for image load attempts to finish. This is bounded so one
# broken image cannot hang the capture forever.
deadline = time.monotonic() + 30
while time.monotonic() < deadline:
pending = driver.execute_script(
"return Array.from(document.images).filter(img => !img.complete).length;"
)
if pending == 0:
break
time.sleep(0.25)
new_height = driver.execute_script(
"return Math.max(document.body.scrollHeight, "
"document.documentElement.scrollHeight);"
)
if new_height == previous_height:
stable_passes += 1
else:
stable_passes = 0
previous_height = new_height
failed = driver.execute_script(
"return Array.from(document.images)"
".filter(img => img.complete && img.naturalWidth === 0)"
".map(img => img.currentSrc || img.src);"
)
if failed:
print(f"Warning: {len(failed)} image(s) completed unsuccessfully:")
for src in failed:
print(f" {src}")
# Return to the top for a predictable final page state, then capture
# the full document (Firefox-specific API).
driver.execute_script("window.scrollTo(0, 0);")
time.sleep(0.2)
driver.save_full_page_screenshot(output_path)
print(f"Saved full-page PNG to {output_path}")
finally:
driver.quit()
if __name__ == "__main__":
if len(sys.argv) != 3:
raise SystemExit("Usage: python full_page.py URL OUTPUT.png")
capture(sys.argv[1], sys.argv[2])
How the wait works
document.imagescovers image elements in the document. It does not guarantee that CSS background images, canvas drawings, video frames, or content inside cross-origin frames are ready.img.completemeans the image load attempt completed, including failed attempts.naturalWidth > 0is a useful success check for ordinary raster images.- The bounded 30-second image wait prevents an indefinitely pending image from stalling the job. Adjust this budget for the pages you capture; it is not a guarantee that a site has finished all visual updates.
- The repeated pass handles some height changes caused by lazy loading or inserted content. It is practical defensive logic, not a universal signal that a dynamic page has settled.
- The short pause after each scroll gives scroll-triggered code a chance to run. Some sites need a longer pause or a site-specific condition.
Options and variations
Wait for a particular image or selector
When a known image matters more than every image on the page, target it directly. For example, after navigation you can wait for a selector and then check its image:
from selenium.webdriver.support.ui import WebDriverWait
image = WebDriverWait(driver, 20).until(
lambda d: d.find_element("css selector", "img.hero")
)
WebDriverWait(driver, 30).until(
lambda d: d.execute_script(
"return arguments[0].complete && arguments[0].naturalWidth > 0", image
)
)
This only verifies the selected image. For pages with delayed app content, wait for a meaningful application selector or state as well. Selenium notes that dynamic content may continue after the document readiness state reaches complete. Selenium waits documentation.
Use page-load readiness deliberately
Selenium’s page-load strategy can be set to normal, eager, or none through browser options. These control when navigation returns; they do not replace the scroll and image checks. Keep the default unless you have a reason to return earlier and implement explicit readiness waits. See Selenium browser options.
Save screenshot bytes
Firefox also documents get_full_page_screenshot_as_png(), which returns PNG bytes. This is useful when the next step is uploading or processing the image in memory:
png_bytes = driver.get_full_page_screenshot_as_png()
with open("page.png", "wb") as image_file:
image_file.write(png_bytes)
What if you need a different browser?
The material cited here documents Firefox’s native full-document method. Selenium’s generic save_screenshot() captures the current window, not automatically the full document. Other browser-specific or stitched approaches have different constraints; verify the active browser and driver documentation and check sticky headers, very tall pages, and dynamically changing layouts before relying on them.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Its one-call endpoint can return an image or PDF. The full Selenium example above gives you browser-level control; use the API when you want a hosted capture request without managing Firefox and a driver. See the ScreenshotNeo API documentation.
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 banners, newsletter popups, and chat widgets are removed before capture; each cleanup step can be turned off.
- Bot checks and CAPTCHAs, blank pages, failed loads, timeouts, and cache hits are not billed. Response headers report the page verdict and billing status.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools for Claude, Cursor, and other MCP clients. - The free plan includes 1,000 screenshots a month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is on every plan.
Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot only shows the visible window | Using the ordinary WebDriver screenshot method or a browser without the Firefox full-page API. | Use Firefox WebDriver’s save_full_page_screenshot(), and confirm the browser and driver actually started as Firefox. |
| Images below the fold are missing | The page was captured without scrolling, or a site-specific loader has not run. | Scroll in viewport steps, wait for relevant image elements, and add a selector or app-specific wait where necessary. |
Some images are broken despite complete being true |
The load attempt ended in failure; completion alone does not establish a valid image. | Check naturalWidth and currentSrc. Investigate the image URL, access controls, or browser console/network failures. |
| The script waits until its timeout | An image remains pending, the page is continuously changing, or an app condition never occurs. | Keep waits bounded, identify the specific pending element, and wait for a page-specific condition instead of requiring every image if that is not needed. |
| Firefox fails to start or reports a driver error | Firefox or its driver is absent, incompatible, or not discoverable. | Install Firefox and use Selenium Manager or configure a compatible driver according to Selenium’s setup documentation. |
| Content is still missing after all image elements complete | The visual content may be CSS backgrounds, canvas, video, an iframe, or JavaScript-driven content. | Wait for the relevant page state and inspect the content type. The document.images check only covers document image elements. |
| Screenshot cuts off or differs around sticky elements | Very tall documents and fixed or sticky UI can behave differently across capture methods. | Check the saved output at the target viewport and test the actual browser/API on representative pages. |
Performance, reliability, and cost
Scrolling and waiting add time proportional to page length, image loading, and any site-specific settling period. Avoid a large fixed sleep at the top: it does not trigger viewport-based lazy loading. Use bounded waits and the narrowest readiness condition that meets the capture’s purpose. Repeated passes help catch some growing-page cases but cannot prove that an application will never change again.
Very long pages can produce large PNG files and consume substantial browser memory. If you control the capture target and need lower transfer or storage size, consider resizing or another output format after capture; test quality and page completeness. For repeat runs, do not assume identical output when ads, personalization, animations, timestamps, or network responses vary.
Local Selenium captures have no per-screenshot API fee, but they use your compute, browser setup, maintenance, and execution time. ScreenshotNeo pricing is 1,000 free shots per month with no card, then Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free. Every feature is on every plan. Choose based on volume and whether hosted capture, clean shots, or MCP access saves you implementation work.
FAQ
Does Selenium’s normal screenshot method capture the whole page?
Do not assume so. The generic method is documented as capturing the current window. Firefox’s Python WebDriver provides explicit full-document PNG methods.
Does waiting for document.readyState equal complete guarantee the images are ready?
No. Lazy images may not be requested until scrolling brings them near the viewport, and dynamic application content can continue after navigation returns.
Can this detect every failed visual asset?
No. Checking document.images and naturalWidth covers ordinary image elements, not every background, canvas, video, or frame resource.
Is the Firefox full-page method guaranteed to match every browser?
No. It is documented by Firefox WebDriver. Verify support and output behavior for the exact browser and driver you deploy.


