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.
Recommended Free Tools
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.
#1 Best Overall
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
- 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.
- Compare SVG and raster output. Try
toSvgon the same node, thentoBlobortoPng. If SVG serialization completes but raster output does not, focus on SVG image loading, image decoding, canvas work, and output dimensions. - 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.
- 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.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallRank #2
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.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsMake 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.
Rank #4
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
pixelRatioor 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
skipAutoScaleonly 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.
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. |
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.
Best Value
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.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Quick Recap
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.

