ScreenshotNeo

BlogHow-to

How to Fix Selenium Taking a Screenshot Before Images Load

Wait for the images your screenshot needs, including lazy-loaded assets, and handle broken images and timeouts explicitly.

By the ScreenshotNeo team4 October 20268 min read

Fix: after navigation, use a bounded Selenium explicit wait for the images your screenshot needs. For ordinary <img> elements, check both img.complete and img.naturalWidth > 0. The first property alone can also be true for a broken or source-less image. If images use lazy loading, scroll the relevant region into view before waiting. Then capture the screenshot.

driver.get() returning is not a guarantee that every image or JavaScript-driven component is ready. Selenium’s normal page-load strategy waits for document readiness to reach complete, but a single-page app can continue rendering after that. Use a condition tied to the visual state you need, not an arbitrary sleep. Selenium waiting strategies · Selenium browser options

Wait for successful image loads in Python

This example waits for all images currently in document.images to complete successfully, then saves a screenshot. It has a 20-second limit so a failed or stalled asset produces a visible timeout instead of hanging indefinitely.

from selenium.webdriver.support.ui import WebDriverWait

# Navigate first:
driver.get("https://example.com")

wait = WebDriverWait(driver, 20)
wait.until(lambda d: d.execute_script("""
  const imgs = [...document.images];
  return imgs.length > 0 && imgs.every(img => img.complete && img.naturalWidth > 0);
"""))

driver.save_screenshot("page.png")

Replace the URL with your target. The condition requires at least one image and treats every image as required. That is useful for a simple page, but may be too strict in production: decorative, irrelevant, broken, or intentionally empty images can prevent the wait from succeeding. Prefer selecting only the assets that appear in the screenshot and reporting failures separately.

Choose the right images and handle failures

If only a particular gallery, hero, or report matters, scope the wait to those elements instead of every image on the page. This example waits for images inside a known container and reports their source URLs if the condition times out.

from selenium.common.exceptions import TimeoutException
from selenium.webdriver.support.ui import WebDriverWait

selector = "main .report-gallery img"

def required_images_ready(driver):
    return driver.execute_script("""
      const imgs = [...document.querySelectorAll(arguments[0])];
      return imgs.length > 0 && imgs.every(img => img.complete && img.naturalWidth > 0);
    """, selector)

try:
    WebDriverWait(driver, 20).until(required_images_ready)
except TimeoutException:
    states = driver.execute_script("""
      return [...document.querySelectorAll(arguments[0])].map(img => ({
        src: img.currentSrc || img.src || "(no source)",
        complete: img.complete,
        naturalWidth: img.naturalWidth
      }));
    """, selector)
    raise RuntimeError(f"Required images did not load: {states}")

driver.save_screenshot("report.png")

A failed image has naturalWidth === 0. Decide whether that should fail the capture or be logged and skipped. If the page legitimately contains no matching images, change the condition to accept an empty set; the example above deliberately treats that as a selector or page-state problem.

Trigger lazy-loaded images before waiting

Images marked with native loading="lazy" may not be requested until they are near the viewport, and lazy loading does not necessarily delay the window load event. Scroll the screenshot region or target images into view first, then wait for their successful image state. MDN: <img> reference

# Scroll through the document in viewport-sized steps to trigger lazy content.
height = driver.execute_script("return document.body.scrollHeight")
step = driver.execute_script("return Math.max(window.innerHeight, 400)")
for y in range(0, height, step):
    driver.execute_script("window.scrollTo(0, arguments[0])", y)

driver.execute_script("window.scrollTo(0, 0)")

# Now wait for the required img elements as shown above.
wait.until(lambda d: d.execute_script("""
  const imgs = [...document.images];
  return imgs.length > 0 && imgs.every(img => img.complete && img.naturalWidth > 0);
"""))

driver.save_screenshot("full-page-content.png")

For a long page, scrolling every viewport may be slower than bringing only the target images into view. You can locate those elements and use Selenium’s scrollIntoView on each before waiting. For a full-page capture, confirm how your browser and screenshot method handle content outside the viewport; a normal viewport screenshot and a full-page screenshot are different capture requirements.

Use a page-specific readiness condition when needed

The document.images collection covers ordinary image elements. It does not prove that CSS background images, canvas drawings, video frames, or an application component have finished rendering. For those cases, wait for the resource or component state that matters: for example, a visible chart-ready marker, a known component becoming visible, or an application-provided completion flag. Selenium explicit waits poll a condition until it succeeds or times out, so the condition can describe your own page’s readiness. Selenium: waiting strategies

# Example: wait for an application-owned marker before capture.
WebDriverWait(driver, 20).until(
    lambda d: d.find_element("css selector", "[data-render-state='ready']").is_displayed()
)
driver.save_screenshot("ready.png")

Use a marker the application sets only after the content in the screenshot is ready. Waiting for a generic element to exist may be insufficient if it appears before its images or data are populated.

Page-load strategy is not screenshot readiness

Selenium’s page-load strategies control when navigation returns; they do not universally guarantee that the visual state you want is complete.

Strategy Navigation behavior Screenshot implication
normal (default) Waits for document readiness to reach complete. Add an explicit wait for required images or app state. JavaScript may still change the page after readiness.
eager Returns when readiness reaches interactive. Images may still be loading; an explicit wait is especially important.
none Does not wait for a document readiness state. Navigation can return very early. Synchronize every required visual condition yourself.

Keep a suitable navigation strategy and add a visual readiness condition. Switching to eager or none can shorten the wait before Selenium proceeds, but does not make capture more reliable by itself. Selenium also notes that navigation waits do not apply in the same way to navigation triggered by clicking or submitting a form; wait explicitly after those actions too. Selenium browser options

cURL, Python, and Node.js alternatives

The synchronization fix above is Selenium code in Python. These examples show how to request a screenshot from a URL when you do not need browser automation in your own process. They do not expose Selenium’s browser session or replace application-specific readiness logic.

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()
open("shot.webp", "wb").write(r.content)

Node.js

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}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

See the ScreenshotNeo API documentation for request options and response details.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. A GET request with a URL returns an image or PDF. Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response reports the page verdict and billing status in headers.

For a URL-only capture, use this one-call request:

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://example.com \
  -o shot.webp

ScreenshotNeo also has an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan.

Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.

Troubleshooting

Symptom Likely cause Fix
The wait passes, but an image is visibly broken. The condition checked only complete, which may also be true for a broken image. Require naturalWidth > 0 for images whose successful load matters. MDN: complete · MDN: naturalWidth
The wait times out on a page that looks ready. An irrelevant or broken image, a source-less element, a selector with no matches, or an asset that never finishes. Scope the selector to required images, log each image’s currentSrc, complete, and naturalWidth, and decide how optional failures should be handled.
Images below the fold are missing. They are lazy-loaded and were never brought near the viewport. Scroll the target region or images into view, then wait for successful loading.
The images are ready, but the screenshot still shows a loading state. Other JavaScript work or app rendering is still in progress. Wait for a page-specific ready marker or component state in addition to the image condition.
A CSS background, canvas, or video frame is missing. That content is not represented by a normal <img> element. Identify and wait for the relevant resource or application state; document.images cannot confirm it.
The screenshot is captured too early after a click or form submission. Navigation waits for driver.get() do not cover every action-triggered navigation or app update. After the action, wait for the resulting URL, component, image state, or application ready marker.

Performance, reliability, and cost

  • Use the smallest correct condition. Waiting for every image can add latency because of unrelated assets. Select only the images that must appear in the capture.
  • Bound every wait. A timeout makes a stalled request or wrong selector diagnosable. Record the failed image sources and state when the wait expires.
  • Avoid fixed sleeps as the main synchronization method. A sleep can be too short on a slow page and waste time on a fast one. Poll the condition you actually need.
  • Do not mix implicit and explicit waits. Selenium warns that the combination can produce unpredictable timeout durations. Use explicit waits for screenshot readiness and keep implicit wait configuration consistent with that approach.
  • Reuse a browser session when capturing multiple pages. Repeatedly starting a driver adds setup work; close the driver when the capture job ends. Page load and image download time still depend on the target site and network.
  • Account for variability. Image delivery, lazy-loading thresholds, app rendering, and browser behavior vary by site. A successful wait proves only the condition you defined, not that every pixel is stable.

For direct Selenium captures, the main cost is the time and resources needed to run the browser and load the target page; the sources cited here do not establish a universal cost or speed benchmark. For managed URL captures, ScreenshotNeo’s free allowance is 1,000 shots a month; listed paid tiers begin at $5 for 3,000. Check the docs for available capture settings and billing response headers.

FAQ

Is document.readyState === "complete" enough?

No. It describes document readiness, not necessarily later JavaScript rendering or every image and component state your screenshot needs.

Can I just increase Selenium’s page-load timeout?

A longer navigation timeout may allow navigation more time, but it does not define when the particular visual content is ready. Add an explicit condition for that content.

Should every broken image fail the screenshot job?

Only if that image is required for the result. Treat required asset failures as capture failures; log or ignore optional assets according to your use case.

Does this wait make screenshots deterministic?

It makes one readiness condition explicit. Animations, changing data, fonts, background images, and other page behavior may need their own conditions or controls.