How to Make PhantomJS Screenshots Wait for Images and Fonts
Wait for images and font requests before rendering a PhantomJS screenshot, with bounded timeouts, failure diagnostics, and a complete example.
To make a PhantomJS screenshot wait for images and fonts, keep image loading enabled, set page.settings.resourceTimeout before page.open(), then wait for page images to finish and for tracked font requests to settle before calling page.render(). Put a hard deadline on the wait and report failed resources. PhantomJS’s page.open() callback is a page-load checkpoint, not a guarantee that every later visual change has finished.
PhantomJS does not document support for the modern document.fonts.ready API. The example below tracks likely font requests through PhantomJS resource callbacks instead. Treat that as a practical signal, not proof that every font has painted: validate it against your PhantomJS build and target pages. The official [PhantomJS screen capture documentation](https://phantomjs.org/screen-capture.html) explains that PhantomJS uses WebKit to render pages.
1. Install PhantomJS and save the script
Install a PhantomJS binary available for your operating system and put it on your PATH. Save the following as capture.js. Run it with phantomjs capture.js https://example.com screenshot.png. The script uses PhantomJS’s callback-based API and ES5-compatible syntax.
var webpage = require('webpage');
var system = require('system');
var url = system.args[1];
var output = system.args[2] || 'screenshot.png';
var deadlineMs = 10000;
var resourceTimeoutMs = 8000;
var pollIntervalMs = 100;
var settleMs = 250;
if (!url) {
console.error('Usage: phantomjs capture.js <url> [output.png]');
phantom.exit(2);
}
var page = webpage.create();
var startedAt = Date.now();
var pendingFonts = {};
var failedResources = [];
var finished = false;
function looksLikeFont(url) {
return /\\.(woff2?|ttf|otf|eot)(?:[?#]|$)/i.test(url);
}
function finish(code) {
if (finished) return;
finished = true;
phantom.exit(code);
}
page.settings.loadImages = true;
page.settings.resourceTimeout = resourceTimeoutMs;
page.onResourceRequested = function (request) {
if (looksLikeFont(request.url)) {
pendingFonts[request.id] = request.url;
}
};
page.onResourceReceived = function (response) {
if (pendingFonts[response.id] && response.stage === 'end') {
delete pendingFonts[response.id];
if (response.status >= 400) {
failedResources.push(response.url + ' (HTTP ' + response.status + ')');
}
}
};
page.onResourceError = function (error) {
failedResources.push(error.url + ' (' + error.errorString + ')');
Object.keys(pendingFonts).forEach(function (id) {
if (pendingFonts[id] === error.url) delete pendingFonts[id];
});
};
page.onResourceTimeout = function (request) {
failedResources.push(request.url + ' (resource timeout)');
Object.keys(pendingFonts).forEach(function (id) {
if (pendingFonts[id] === request.url) delete pendingFonts[id];
});
};
page.open(url, function (status) {
if (status !== 'success') {
console.error('Page load failed: ' + status);
finish(1);
return;
}
var poll = setInterval(function () {
var elapsed = Date.now() - startedAt;
var images = page.evaluate(function () {
return Array.prototype.map.call(document.images, function (img) {
return {
complete: img.complete,
loaded: img.complete && img.naturalWidth > 0,
src: img.currentSrc || img.src
};
});
});
var imagesComplete = images.every(function (image) {
return image.complete;
});
var fontsComplete = Object.keys(pendingFonts).length === 0;
var deadlineReached = elapsed >= deadlineMs;
if ((imagesComplete && fontsComplete) || deadlineReached) {
clearInterval(poll);
var failedImages = images.filter(function (image) {
return image.complete && !image.loaded;
}).map(function (image) {
return image.src + ' (image failed or has no intrinsic width)';
});
failedResources = failedResources.concat(failedImages);
if (deadlineReached && (!imagesComplete || !fontsComplete)) {
console.error('Readiness deadline reached; capturing with resources still pending.');
Object.keys(pendingFonts).forEach(function (id) {
failedResources.push(pendingFonts[id] + ' (still pending at deadline)');
});
}
if (failedResources.length) {
console.error('Resource issues:\n' + failedResources.join('\n'));
}
setTimeout(function () {
page.render(output);
console.log('Saved ' + output);
finish(0);
}, settleMs);
}
}, pollIntervalMs);
});
This waits for DOM images, including images marked complete but failed, and waits for font URLs matching common font file extensions to finish or fail. Failed images do not hold the script open forever; they are reported and the screenshot is still captured. The short settling pause allows a little time for layout and paint after the tracked requests settle. Set deadlineMs and resourceTimeoutMs to suit your workload; keep the overall deadline bounded.
PhantomJS’s resource callbacks are documented as onResourceRequested, onResourceReceived, onResourceError, and onResourceTimeout. See the [PhantomJS page API](https://phantomjs.org/api/webpage/) for the API details. The font URL classifier is intentionally simple: a font served from a URL without a recognizable extension may not be tracked. Adapt the classifier using response headers or known font URL patterns for your site.
2. Understand what the wait covers
| Wait signal | What it tells you | Limit |
|---|---|---|
page.open() callback |
The initial page load has reached its load-finished checkpoint. | Scripts, lazy images, and font swaps can still affect the appearance afterward. |
img.complete |
The image element has completed loading or failed. | It does not mean success; check naturalWidth to identify many failures. CSS background images are not in document.images. |
| Tracked font requests | Recognized font URLs have completed or errored. | A request completing does not confirm that the intended font rendered. Fonts may have extensionless URLs or be served from CSS/data URLs. |
| Settling interval | Allows a brief pause after the current checks pass. | A fixed pause cannot guarantee readiness if scripts trigger later changes. |
For pages with lazy loading, scrolling can trigger additional image requests. The script above does not scroll the page, so it waits only for image elements present and requested in the current document state. For full-page captures where lazy content matters, add a page-specific scroll-and-wait phase before the final readiness check, with the same hard deadline.
Font request tracking is operational guidance inferred from the available PhantomJS callbacks. The retrieved PhantomJS documentation does not establish support for document.fonts.ready; do not assume that modern browser API works in your runtime. A font may also load successfully but fall back because the page’s CSS, font format, cross-origin access, or PhantomJS/WebKit build prevents its use.
3. Use cURL, Python, or Node.js to run the script
PhantomJS itself is launched as a command-line program. These examples download a remote copy of capture.js only if you have hosted your own script at a URL; replace the example URL with that location. For local use, run the command in section 1 directly.
cURL
curl -fsS https://example.com/capture.js -o capture.js
phantomjs capture.js https://example.com output.png
Python
import subprocess
result = subprocess.run(
["phantomjs", "capture.js", "https://example.com", "output.png"],
check=False,
capture_output=True,
text=True,
timeout=30,
)
print(result.stdout)
print(result.stderr)
if result.returncode != 0:
raise SystemExit(result.returncode)
Node.js
const { spawn } = require('node:child_process');
const child = spawn('phantomjs', ['capture.js', 'https://example.com', 'output.png']);
child.stdout.on('data', data => process.stdout.write(data));
child.stderr.on('data', data => process.stderr.write(data));
child.on('close', code => {
if (code !== 0) process.exitCode = code;
});
Use an outer process timeout as well as the script’s readiness deadline. That protects a job runner from hangs in the runtime itself. Avoid printing secrets if you add authenticated headers or cookies.
4. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot starts before images finish | page.render() runs immediately inside the open callback, or loadImages was disabled. |
Set page.settings.loadImages = true before page.open() and wait for image completion. |
| Wait never completes | A broken image, pending resource, or logic that treats failures as unfinished. | Use a hard deadline; treat complete as settled even when naturalWidth is zero, then report the failure. |
| Font is still the fallback face | Font request is not recognized, fails, or loads too late for the chosen settle interval; runtime support or font format may differ. | Inspect resource callback URLs and errors, check the target’s CSS and font hosting, and validate with the exact PhantomJS binary. Increase the bounded settle interval if needed. |
| Lazy images are missing | They were not requested because the page never scrolled to them. | Scroll through the relevant content, wait for newly created image elements, and retain an overall deadline. |
Some images are absent despite complete |
The requests failed, were blocked, or produced an unusable image. | Check naturalWidth, onResourceError, HTTP status, URL access, and resource timeout diagnostics. |
| Script errors at startup | PhantomJS version/API mismatch or unsupported JavaScript syntax. | Use the callback-oriented ES5 pattern shown here; confirm the binary version and avoid assuming modern Promise or browser APIs. |
| Capture exits but output is missing | Invalid output path, permissions, or unsupported output format. | Use a writable path and a supported image filename such as PNG; check process stderr. |
5. Performance, reliability, and cost
Readiness polling avoids waiting the same long fixed delay on every page: fast pages can proceed as soon as checks pass, while slower pages get time up to the deadline. Polling every 100 milliseconds is usually a modest check, but each poll crosses into the page context. Raise the interval for large batches if that overhead matters. A fixed settling pause is simple but wastes time on fast pages and can still be too short on slow ones.
Set the resource timeout before the initial open call, and set an independent overall deadline for all readiness checks. Resource failures should be recorded and should not block capture forever. If complete image coverage matters, account for CSS backgrounds, lazy loading, frames, and page scripts separately; document.images does not represent every visual resource.
PhantomJS is a legacy rendering runtime. Pages that depend on newer browser behavior may render differently or fail, so validate important captures with your exact binary and target pages. Running locally means you manage the runtime and its resource use; cost depends on your infrastructure and maintenance. There is no benchmark or universal wait duration that fits every site.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server from [ScreenshotNeo](https://screenshotneo.com). One GET request returns a PNG, JPEG, WebP, or PDF. Its capture flow accepts cookie and consent banners and removes 60+ known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server offers take_screenshot, get_page_info, and capture_pdf tools for AI agents.
See the [ScreenshotNeo API documentation](https://screenshotneo.com/docs/) for options and request details.
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}`);
1,000 screenshots a month are free with no card; paid plans start at $5 for 3,000. [Create a free ScreenshotNeo account](https://screenshotneo.com/account/sign-up/) to make your first request.
FAQ
Does page.open() wait for every image and font?
No. Treat its callback as the initial load-finished checkpoint and add explicit readiness checks for the resources that matter to your capture.
Can I use document.fonts.ready in PhantomJS?
The cited PhantomJS documentation does not establish support for that modern API. Track font requests and verify the result in the runtime you actually use.
Should a failed image prevent the screenshot?
Usually not. Let the image count as settled, report its failure, and capture at the deadline or when other readiness conditions pass.
Is a fixed delay enough?
It can work for a known, stable page, but it may be too short under slow conditions and unnecessarily long under fast ones. A bounded readiness check gives more useful diagnostics.


