Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Wait for every image that matters to finish loading and decoding before you call html2canvas. Use HTMLImageElement.decode() when available, reject or deliberately handle failures, then await the promise returned by html2canvas. Do not treat img.complete alone as proof of success: it is also true for broken images and images with no source.

The reliable capture sequence

A deterministic capture has three separate waits:

  1. Make required lazy images eligible to load (for example, scroll them into view).
  2. Wait until each target image has loaded and decoded successfully.
  3. Call html2canvas and await its rendering promise before exporting the canvas.

The readiness check must cover the same content you intend to capture. If your target includes content inserted later, run the check after that DOM change and immediately before the capture.

A production-ready JavaScript helper

This helper prefers decode(), validates already-complete images with naturalWidth, and falls back to load/error events in older environments.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
async function waitForImages(root) {
  const images = [...root.querySelectorAll("img")];

  await Promise.all(images.map(async (img) => {
    // complete can also mean broken or source-less, so validate naturalWidth.
    if (img.complete && img.naturalWidth > 0) {
      if (typeof img.decode === "function") {
        await img.decode();
      }
      return;
    }

    // decode() waits for a usable decoded image and rejects on failure.
    if (typeof img.decode === "function") {
      await img.decode();
      return;
    }

    // Older-browser fallback.
    await new Promise((resolve, reject) => {
      img.addEventListener("load", resolve, { once: true });
      img.addEventListener(
        "error",
        () => reject(new Error(`Image failed: ${img.currentSrc || img.src}`)),
        { once: true }
      );
    });

    if (img.naturalWidth === 0) {
      throw new Error(`Image is not usable: ${img.currentSrc || img.src}`);
    }
  }));
}

async function capture(element) {
  await waitForImages(element);
  const canvas = await html2canvas(element, {
    imageTimeout: 15000
  });
  return canvas;
}

const element = document.querySelector("#report");
if (!element) throw new Error("Capture target #report was not found");

try {
  const canvas = await capture(element);
  const pngUrl = canvas.toDataURL("image/png");
  document.querySelector("#preview").src = pngUrl;
} catch (error) {
  console.error("Capture failed", error);
}

The documented imageTimeout default is 15,000 milliseconds; setting it to 0 disables that timeout. A timeout is only a limit on waiting, not a guarantee that an image loaded successfully. Check the configuration for the html2canvas version installed in your project because options can vary by release.

Choose what “ready” means for your page

Waiting for every image is safest, but not always necessary. Define the scope and failure policy explicitly.

Decision Strict capture Best-effort capture
Scope All img descendants of the target, plus any externally inserted content you include Only images essential to the result; optional thumbnails may be skipped
Failure Reject immediately and report the failed URL Log the failure and continue, or replace the image with a known fallback
Use case Invoices, archival screenshots, compliance evidence Dashboards or feeds where one optional image should not block the whole view

The sample code uses the strict policy. To continue after an optional failure, wrap each image wait in a try/catch, record the URL, and resolve only for images your application marks as nonessential. Do not silently hide failures when pixel accuracy matters.

Why common shortcuts fail

window.onload and DOMContentLoaded

These events describe document lifecycle milestones, not the readiness of images added later by JavaScript. They also do not guarantee that lazy images outside the viewport have even started a request.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A fixed delay

setTimeout(...) guesses at network and decode time. It can be unnecessarily slow on a fast connection and still too short on a slow one. An image-specific promise gives you a meaningful condition instead.

img.complete by itself

MDN documents that complete is true when the image has finished fetching, but also when it is broken or has no source. Pair it with naturalWidth > 0, and prefer decode() for usable decoded pixels.

Rank #2
Sale
HTML and CSS: Design and Build Websites
  • HTML CSS Design and Build Web Sites
  • Comes with secure packaging
  • It can be a gift option

Calling html2canvas without awaiting it

html2canvas returns a promise. Exporting or reading the canvas before that promise resolves can produce incomplete output or race with rendering. Always use const canvas = await html2canvas(element, options).

Lazy-loaded images: start the request first

An image with loading="lazy" may not request its resource until it approaches the viewport. Before waiting, make required images eligible. The simplest browser-side approach is to scroll the target into view and allow the browser a frame to schedule loading:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
element.scrollIntoView({ block: "center" });
await new Promise(requestAnimationFrame);
await waitForImages(element);

For a long page, you may need to scroll through the capture region or temporarily change the page’s loading strategy. Re-run the check after any code that changes src, srcset, image visibility, or the target DOM.

Cross-origin images and canvas security

Loading and decoding are separate from permission to use pixels. A cross-origin image can load successfully yet be omitted by html2canvas or taint the canvas, preventing operations such as toDataURL(). The remote server must allow the request through CORS, or you must use a configured proxy.

Set useCORS: true only when the image server sends an appropriate CORS response. The proxy option is another documented route for fetching resources through a same-origin service. allowTaint: true is not an export fix: an origin-tainted canvas remains unreadable. Configure the server or proxy instead.

const canvas = await html2canvas(element, {
  useCORS: true,
  imageTimeout: 15000
});

CORS headers must be present on the image response, not merely on your HTML page. If credentials are involved, the server’s credential and origin headers must also match your request mode.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Debugging checklist

Find the actual images

const images = [...element.querySelectorAll("img")];
console.table(images.map(img => ({
  src: img.currentSrc || img.src,
  complete: img.complete,
  naturalWidth: img.naturalWidth,
  naturalHeight: img.naturalHeight,
  loading: img.loading
}))); 

If the screenshot includes a portal, shadow-root content, or a node outside the selected subtree, include that content in your readiness logic or capture a containing element.

Decode rejects

A rejected decode() normally means the resource is missing, corrupted, unsupported, or no longer available. Log currentSrc, verify the URL in the browser network panel, and decide whether to fail, omit, or substitute it.

The image is present but blank

Check naturalWidth, CSS visibility, responsive srcset selection, and whether a lazy-loading trigger occurred. A successful decode does not override CSS that hides the element.

The canvas export throws a security error

This is usually an origin-taint problem, not a timing problem. Confirm CORS response headers or route the image through a properly configured proxy. Waiting longer cannot change browser security rules.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #4
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • Brand: Wiley
  • Set of 2 Volumes
  • A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers

The result differs from the browser

html2canvas reconstructs a representation from DOM and supported CSS; it does not capture the browser’s actual pixels. Unsupported CSS, filters, fonts, canvas size limits, and layout changes can affect fidelity independently of image readiness.

Performance and reliability practices

  • Limit scope: query only the images inside the capture target instead of scanning the whole document.
  • Wait in parallel: Promise.all avoids serial network waits while still enforcing an all-images policy.
  • Use a timeout policy: retain html2canvas’s 15-second image timeout unless your application has a justified alternative; disabling it with 0 can leave a capture waiting indefinitely.
  • Capture once the DOM is stable: stop animations or mutation-driven updates that can replace image sources after your readiness check.
  • Record failures: include the URL and error type in logs so operators can distinguish a missing asset from CORS or rendering limitations.
  • Test the installed version: options and browser behavior can differ across html2canvas releases.

Alternative implementations

Event-only helper

When decode() is unavailable, wait for either load or error, then verify dimensions:

function waitForLoadOrError(img) {
  if (img.complete) {
    return img.naturalWidth > 0
      ? Promise.resolve()
      : Promise.reject(new Error("Image is broken or empty"));
  }
  return new Promise((resolve, reject) => {
    img.addEventListener("load", () => {
      if (img.naturalWidth > 0) resolve();
      else reject(new Error("Image loaded with no usable pixels"));
    }, { once: true });
    img.addEventListener("error", () => reject(new Error(img.src)), { once: true });
  });
}

Filtering essential images

const required = [...element.querySelectorAll("img[data-required]")];
await Promise.all(required.map(img => img.decode()));

Use a deliberate marker such as data-required; do not rely on incidental class names that may change with a redesign.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

For server-side or repeatable captures, ScreenshotNeo provides a website screenshot API and MCP server. One request returns PNG, JPEG, WebP, or PDF. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Read the ScreenshotNeo API documentation for all options. A minimal call is:

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

The same request in Python:

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)

And Node.js:

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(`HTTP ${res.status}`);
const buffer = Buffer.from(await res.arrayBuffer());
require('fs').writeFileSync('shot.webp', buffer);

ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. Every feature is included on every plan; 1,000 screenshots per month are free with no card, and paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Should I wait for fonts as well as images?

If text metrics affect the layout, wait for document.fonts.ready separately; image readiness does not guarantee stable font rendering.

Can I capture a failed image as an empty box?

Yes. Adopt a best-effort policy, replace the source with a controlled placeholder, and resolve the wait only after that replacement is decoded.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Does waiting improve unsupported CSS?

No. Readiness prevents missing image resources; it cannot add CSS features that html2canvas does not implement.