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.

If html-to-image stops partway through a batch and neither then() nor catch() runs, make each capture observable and time-bounded, then isolate the resource or browser stage that is stalling. Start with sequential captures, a small control element, and explicit logging. Fonts, external images, inactive-tab scheduling, and large canvas output are common things to investigate; a timeout keeps one unresolved capture from blocking the rest of your batch, but it does not cancel the underlying work.

Why a loop can appear to hang

html-to-image returns promises from its public output methods, including toPng, toSvg, toJpeg, toBlob, toCanvas, and toPixelData. To create an image, it clones the node, copies computed styles, embeds fonts and image assets, serializes the clone into SVG using <foreignObject>, and may rasterize that SVG through an off-screen canvas. Each stage can involve browser scheduling, network work, decoding, or memory-intensive rendering.

That sequence is a useful diagnostic model, not proof that every stalled capture has the same cause. A promise that never settles differs from a normal rendering error: a rejected promise can be caught, while an unresolved one can leave an await waiting indefinitely. If one item in a loop does not settle, later items may never start.

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

Make the failing item observable and bounded

Record the item index and elapsed time around every capture. Begin with sequential processing so you can identify the first problematic item without adding memory pressure from parallel work. Wrap each capture in try/catch and give it an application-level timeout.

import * as htmlToImage from 'html-to-image';

const results = [];
const failures = [];
const PLACEHOLDER_DATA_URL =
  'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';

function withTimeout(promise, ms, index) {
  let timer;
  const timeout = new Promise((_, reject) => {
    timer = setTimeout(
      () => reject(new Error(`html-to-image timeout at item ${index} after ${ms} ms`)),
      ms
    );
  });
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
}

async function renderOne(node, index, timeoutMs = 30000) {
  const started = performance.now();
  console.debug('capture started', { index, at: new Date().toISOString() });

  try {
    const blob = await withTimeout(
      htmlToImage.toBlob(node, {
        cacheBust: false,
        pixelRatio: 1,
        imagePlaceholder: PLACEHOLDER_DATA_URL,
      }),
      timeoutMs,
      index
    );

    if (!blob) throw new Error('html-to-image returned no Blob');
    console.debug('capture completed', {
      index,
      ms: Math.round(performance.now() - started),
      bytes: blob.size,
    });
    return blob;
  } finally {
    // Dispose of caller-created temporary nodes, object URLs, and listeners here.
  }
}

async function renderBatch(nodes) {
  for (let index = 0; index < nodes.length; index += 1) {
    try {
      results[index] = await renderOne(nodes[index], index);
    } catch (error) {
      failures.push({ index, message: String(error) });
      console.error('capture failed', { index, error });
    }
  }
  return { results, failures };
}

Choose the timeout from measurements in your own browser and workload; 30 seconds in this example is a policy value, not a library guarantee or universal fix. A timed-out promise in Promise.race is still running if the library operation itself never settled. Do not immediately launch unlimited replacements: abandoned work can continue consuming CPU or memory. Track timed-out items, limit the number of in-flight renders, and consider restarting or moving the batch if stuck work accumulates.

Always record a settled success, error, or timeout for each item. Clean up temporary DOM nodes, object URLs, image elements, and event listeners your own code created, including after failures. For debugging, log before and after each caller-controlled preparation step as well as the capture, so you can tell whether the wait occurs before the library is called or inside its rendering pipeline.

Isolate the stage that stalls

  1. Run a control capture. Use a small same-origin node with no web fonts, external images, CSS background images, nested canvas, or other complicated assets. If it succeeds consistently, add one resource category at a time.
  2. Compare SVG and raster output. Try toSvg on the same node, then toBlob or toPng. If SVG serialization completes but raster output does not, focus on SVG image loading, image decoding, canvas work, and output dimensions.
  3. Keep the browser and package fixed while reproducing. Note the exact browser, package version, whether the tab is visible, the failing item index, and the node dimensions. Change one factor at a time.
  4. Reduce the input. Temporarily remove fonts, images, backgrounds, and large sections until the capture settles. Restore components individually to find the dependency that changes the result.

A published report describes a batch of roughly 300 tables that stopped at an unresolved promise. That is one user’s example, not evidence of a general failure threshold. A timer-based retry may let a particular batch proceed, but adding a fixed delay does not identify the cause and is not a universal repair.

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

Check inactive tabs and version-specific behavior

Reproduce the problem with the page in the foreground and then, if relevant, with the tab inactive. An issue report for html-to-image 1.11.12 and 1.11.13 describes deferred generation in an inactive tab when requestAnimationFrame was paused; the reporter said work ran after the tab became active and temporarily downgraded to 1.11.11. Treat that downgrade only as a compatibility experiment. Verify the current upstream release and test your actual browser before pinning a version.

If the workflow must keep running while a tab is backgrounded, consider running it in a foreground context or using a worker or server renderer that does not depend on paused page animation frames. Validate the behavior of any chosen approach in your target environment rather than assuming background scheduling is identical across browsers.

Reduce font and image dependencies

Reuse font embedding work

Font embedding is active work: the library scans @font-face rules, fetches font files, base64-encodes them, and adds CSS to the clone. For a stable set of elements, obtain the embedded font CSS once with getFontEmbedCSS() and pass the resulting string as fontEmbedCSS in subsequent captures. If a font provider publishes several formats, setting one suitable preferredFontFormat can avoid considering formats you do not need.

Check that every font URL is valid and that the CSS rules resolve to actual font declarations. A reported Firefox 135.0.1 problem with html-to-image 1.11.12 involved normalizeFontFamily receiving an undefined font during embedding. If removing fonts or supplying cached font CSS resolves the stall, keep that reduced path while you investigate the stylesheet or browser compatibility issue.

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

Make image loading predictable

Image elements and CSS background images are also fetched and embedded during cloning. Before capture, wait for caller-owned images to load and decode; use stable URLs and ensure cross-origin servers provide appropriate CORS headers when those assets must be read. If a nonessential asset fails, imagePlaceholder can provide a fallback, but log the failed asset so a partial image is not mistaken for a complete one.

The library documents cacheBust and imagePlaceholder. Use cacheBust: true only when cache invalidation is needed. Otherwise test with it disabled so URLs remain stable; an issue report about background-image failures also describes a case where disabling cache busting helped. This is a diagnostic possibility, not a guarantee that cache busting causes every image problem.

Control DOM size, output dimensions, and batch concurrency

Before each render, measure the node’s width and height, approximate element count, and estimated output pixels. Cloning and serializing a large subtree, embedding its assets, and rasterizing the result all consume time and memory. A long batch that retains every base64 data URL can add further memory pressure.

  • Start with one capture at a time. Increase concurrency only after measuring completion time and memory use; use a fixed small limit rather than starting the whole batch at once.
  • Lower pixelRatio or capture dimensions for batch work when the output does not need full resolution. Fewer output pixels can reduce canvas pressure.
  • Split very large nodes into smaller sections if the output permits it, and release results or temporary resources as soon as the application no longer needs them.
  • Use skipAutoScale only after checking the result. It can preserve requested dimensions for oversized content but may crop or omit parts of the image.
  • For very large DOMs, consider documented data-URI size limits as well as canvas limits; avoid holding unnecessary encoded copies of the same output.

There is no universal safe node size, pixel ratio, timeout, or concurrency level established for all browsers and pages. Measure the actual workload, including its assets and output format, and set limits from observed latency and memory behavior.

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.

Troubleshooting by symptom

Symptom Likely area to check Next action
One item never logs completion or rejection Unresolved font/image work, browser scheduling, SVG loading, or rasterization Use the timeout and stage logs; reproduce that item alone and remove resource classes one by one.
Capture resumes when the tab becomes active Background-tab scheduling in the tested browser/package combination Reproduce in foreground, record versions, and test a current release; do not treat a downgrade as a confirmed general fix.
Removing fonts changes the result Font URL, malformed or incomplete font CSS, or browser-specific embedding behavior Validate font rules and URLs, test cached fontEmbedCSS, and check the exact browser/package versions.
Removing a background image changes the result Asset fetch, CORS, URL stability, or cache-busting behavior Check the request and response headers, wait for the image, and compare stable URLs with cacheBust: false.
Small captures work; large ones stall or fail DOM size, output pixels, canvas pressure, or encoded data size Lower dimensions or pixelRatio, split the content, and avoid retaining duplicate data URLs.
More parallel jobs make the batch less reliable Concurrent memory or CPU pressure Return to sequential processing, then raise a measured concurrency limit gradually.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

When to render outside the page

A browser-side library is a reasonable fit when the page is available, its assets are controlled, and client-side rendering meets the workflow’s needs. If hundreds of captures must run unattended, the tab may be inactive, or third-party assets are unreliable, a server-side or hosted renderer may reduce the amount of browser lifecycle management in your application. Assess security, licensing, latency, and data handling for the specific service and content; do not assume a hosted option is a drop-in replacement for rendering an arbitrary in-memory DOM node.

Or skip the browser setup

If the page you need is addressable by URL, ScreenshotNeo can capture it through one GET request; this is a hosted page screenshot, not a direct capture of a DOM node held only in your app. Its pre-capture cleanup accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Those steps can each be turned off. Bot checks/CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and responses say which page verdict applied and whether it was billed. An MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents and MCP clients.

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

See the ScreenshotNeo API documentation for the request options and setup. It offers 1,000 screenshots a month free with no card; paid plans start at $5 for 3,000, and every feature is on every plan. Learn about ScreenshotNeo and sign up for 1,000 free screenshots a month with no card.

Choose the fix from the evidence

Keep the smallest reliable path: observable per-item results, a timeout policy, sequential or measured bounded concurrency, and only the font and image work the output actually needs. Use the failing item and the SVG-versus-raster comparison to direct the next test. If the workload depends on background execution or unreliable external pages, move it to a rendering context designed for that operating condition rather than relying on a longer delay in the loop.

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

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.