PhantomJS Screenshots Are Blank: Causes and Fixes
Fix blank, transparent, or incomplete PhantomJS screenshots with a practical diagnosis for load status, timing, dimensions, errors, and HTTPS.
A blank PhantomJS screenshot usually comes from one of five causes: page.open failed, the page was captured before its content rendered, viewportSize or clipRect excluded the content, the image is transparent, or the page/runtime produced network or JavaScript errors. Check those in that order.
PhantomJS reports whether loading finished with success or fail; a file being created does not prove that it contains a usable page. The project’s latest stable release is 2.1 and development is suspended, so modern sites may require a maintained browser automation tool.
1. Start with a status check
Use the callback from page.open and render only when the status is success. This is the smallest useful diagnostic script:
var page = require('webpage').create();
var system = require('system');
var url = system.args[1] || 'https://example.com';
page.open(url, function (status) {
console.log('Status: ' + status);
if (status === 'success') {
page.render('capture.png');
console.log('Saved capture.png');
} else {
console.error('Page did not load; no screenshot rendered.');
}
phantom.exit();
});
The WebPage open API documents the completion callback and its success/fail values. The Quick Start also checks the status before rendering.
What a fail status means
- Confirm the URL includes its scheme, such as
https://. - Open the same URL from the capture machine with a command-line HTTP client.
- Check DNS, firewall, proxy, TLS and certificate problems.
- Inspect resource requests and JavaScript errors using the diagnostics below.
- Do not tune screenshot dimensions until the page can load successfully.
2. Wait for the page’s content, not just the initial load event
A successful page.open callback means the load operation completed. It does not guarantee that a single-page application has finished rendering data, images, fonts or client-side components. The PhantomJS homepage example waits briefly before calling page.render; a fixed delay is useful for a quick test but is not a universal readiness signal.
Use a page-specific readiness condition
When possible, wait until an element that proves the application is ready exists. The following polling pattern is an implementation technique; choose a selector and timeout that match your page.
var page = require('webpage').create();
var system = require('system');
var url = system.args[1] || 'https://example.com';
var readySelector = system.args[2] || '#app-ready';
var deadline = Date.now() + 15000;
function checkReady() {
var ready = page.evaluate(function (selector) {
var element = document.querySelector(selector);
return !!element && element.offsetWidth > 0 && element.offsetHeight > 0;
}, readySelector);
if (ready) {
page.render('capture.png');
console.log('Ready element found; saved capture.png');
phantom.exit();
return;
}
if (Date.now() >= deadline) {
console.error('Timed out waiting for ' + readySelector);
phantom.exit(1);
return;
}
window.setTimeout(checkReady, 250);
}
page.open(url, function (status) {
console.log('Status: ' + status);
if (status !== 'success') {
phantom.exit(1);
return;
}
checkReady();
});
If there is no reliable selector, use a short delay only as a diagnostic and then replace it with a condition tied to the page. Long delays increase capture time without proving that the page is ready; short delays can capture a skeleton or empty application shell.
Check animations and lazy content
- Wait until the content is visible rather than waiting an arbitrary number of seconds.
- If an animation changes the final pixels, capture after the animation’s end state or disable it with page CSS.
- Scroll or trigger lazy-loading behavior before rendering when the required content is below the fold.
- Log the readiness state immediately before
page.renderso a timeout is distinguishable from an empty result.
3. Verify viewport and clipping dimensions
viewportSize sets the headless browser viewport. clipRect limits the rendered rectangle. A zero, negative, incorrectly positioned or overly small clip can produce a blank image even when the document is visible. The screen-capture documentation describes both settings.
var page = require('webpage').create();
page.viewportSize = { width: 1366, height: 768 };
page.open('https://example.com', function (status) {
if (status !== 'success') {
phantom.exit(1);
return;
}
// First test without a clip rectangle.
page.render('full-viewport.png');
// Then add a known, positive rectangle if you need a crop.
page.clipRect = { top: 0, left: 0, width: 1200, height: 700 };
page.render('clipped.png');
phantom.exit();
});
| Setting | What it controls | Diagnostic check |
|---|---|---|
viewportSize |
The browser’s visible width and height | Use positive values and confirm responsive content appears at that size |
clipRect.top/left |
Where the output rectangle begins | Start at 0,0 while debugging |
clipRect.width/height |
The output rectangle’s dimensions | Remove the clip, then reintroduce a known positive rectangle |
If the unclipped viewport image works, your clip rectangle is the likely cause. If the viewport image is blank too, continue with load, timing, transparency and error checks.
4. Rule out a transparent image
PhantomJS does not choose a page background. If the document does not set one, the output can remain transparent. Some image viewers show transparency as white or checkerboard, which can look like an empty page. The PhantomJS FAQ documents this behavior.
page.open('https://example.com', function (status) {
if (status !== 'success') {
phantom.exit(1);
return;
}
page.evaluate(function () {
document.body.bgColor = 'white';
});
page.render('opaque.png');
phantom.exit();
});
Set the background only after the body exists. If the page still looks empty, inspect the PNG’s alpha channel or place it over a contrasting background. A transparent result is different from a page that failed to load.
5. Capture network and JavaScript diagnostics
When status, timing and geometry look correct, inspect what PhantomJS requested and what the page reported.
var page = require('webpage').create();
var system = require('system');
var url = system.args[1] || 'https://example.com';
page.onResourceRequested = function (request) {
console.log('REQUEST ' + request.method + ' ' + request.url);
};
page.onResourceReceived = function (response) {
if (response.stage === 'end') {
console.log('RESPONSE ' + response.status + ' ' + response.url);
}
};
page.onError = function (message, trace) {
console.error('PAGE ERROR: ' + message);
trace.forEach(function (item) {
console.error(' at ' + item.file + ':' + item.line);
});
};
page.onConsoleMessage = function (message, line, source) {
console.log('CONSOLE ' + source + ':' + line + ' ' + message);
};
page.open(url, function (status) {
console.log('FINAL STATUS ' + status);
if (status === 'success') {
page.render('diagnostic.png');
}
phantom.exit(status === 'success' ? 0 : 1);
});
The Quick Start explains that page console messages are not shown by default and demonstrates wiring console output. The troubleshooting guide recommends request logging and calls out TLS, OpenSSL and proxy checks.
HTTPS and OpenSSL
If HTTP loads but HTTPS fails, verify that the PhantomJS process can find compatible OpenSSL libraries and that the certificate chain is accepted in the capture environment. Fix the runtime or certificate installation before changing page code.
Windows proxy behavior
A default Windows proxy can add major latency or prevent resources from loading. Where that matches your environment, try the documented workaround:
phantomjs --proxy-type=none capture.js https://example.com
Use this as a targeted check. A proxy may be required in your network, so do not disable it blindly.
6. Confirm which PhantomJS you are running
Multiple installations can make a script invoke a different binary from the one you upgraded or configured.
phantomjs --version
which phantomjs
On Windows, use where phantomjs to list matching executables. Record the path and version in your job logs. The PhantomJS repository identifies 2.1 as the latest stable release and says development is suspended. The project homepage also displays the suspended-development notice and describes its QtWebKit-based engine.
Do you need X11 or Xvfb?
The FAQ says PhantomJS 1.4 and earlier require an X server; Xvfb is the historical workaround. PhantomJS 1.5 and later are pure headless and do not need X11/Xvfb. Installing Xvfb is therefore not a general fix for a current 2.x installation.
7. A complete diagnostic script
This script combines status logging, a viewport, a white background, console and page-error reporting, and a short readiness wait. Replace readySelector with an element that your page creates when its data is ready.
var page = require('webpage').create();
var system = require('system');
var url = system.args[1] || 'https://example.com';
var readySelector = system.args[2] || 'body';
var output = system.args[3] || 'capture.png';
var timeoutMs = 15000;
var started = Date.now();
page.viewportSize = { width: 1366, height: 768 };
page.onResourceRequested = function (request) {
console.log('REQUEST ' + request.method + ' ' + request.url);
};
page.onError = function (message, trace) {
console.error('PAGE ERROR: ' + message);
trace.forEach(function (item) {
console.error(' at ' + item.file + ':' + item.line);
});
};
page.onConsoleMessage = function (message, line, source) {
console.log('CONSOLE ' + source + ':' + line + ' ' + message);
};
function finish(code) {
phantom.exit(code);
}
function waitForReady() {
var ready = page.evaluate(function (selector) {
var node = document.querySelector(selector);
return !!node && node.offsetWidth > 0 && node.offsetHeight > 0;
}, readySelector);
if (ready) {
page.evaluate(function () {
if (document.body) document.body.bgColor = 'white';
});
page.render(output);
console.log('Rendered ' + output);
finish(0);
return;
}
if (Date.now() - started > timeoutMs) {
console.error('Timed out waiting for ' + readySelector);
finish(1);
return;
}
window.setTimeout(waitForReady, 250);
}
page.open(url, function (status) {
console.log('Status: ' + status);
if (status !== 'success') {
finish(1);
return;
}
waitForReady();
});
phantomjs diagnostic.js https://example.com body capture.png
8. Troubleshooting checklist
| Symptom | Likely cause | Fix |
|---|---|---|
| File is created but entirely blank | page.open returned fail |
Log status; investigate URL, DNS, TLS, proxy and resource failures |
| Page shell appears, data is missing | Capture happened before client-side rendering | Wait for a page-specific selector or state, then render |
| Only a small empty area is saved | Incorrect clipRect |
Remove the clip; verify positive coordinates and dimensions |
| Image is white or checkerboard in a viewer | Transparent page background | Set document.body.bgColor after load and inspect alpha |
| HTTPS fails while HTTP works | OpenSSL or certificate problem | Check runtime libraries and the certificate chain |
| Capture hangs or is extremely slow on Windows | Default proxy intercepting requests | Test --proxy-type=none when appropriate |
| Expected console errors are invisible | PhantomJS does not print page console messages by default | Attach page.onConsoleMessage |
| Different machines produce different results | Different PhantomJS binaries or versions | Log phantomjs --version and executable paths |
| Modern site never becomes usable | Unsupported browser features or suspended runtime | Confirm compatibility; migrate to a maintained browser stack when required |
9. Reliability, performance and maintenance
Reliability
- Return a non-zero process status when loading or readiness fails so schedulers can retry or alert.
- Record the URL, PhantomJS version, viewport, clip rectangle, status and elapsed time for every capture.
- Use a readiness condition with a bounded timeout; otherwise a page that never finishes can hold a worker indefinitely.
- Keep diagnostic request and console logs available for failed jobs, while reducing verbosity for successful high-volume runs.
Performance
- Large viewports, many resources and long readiness waits increase memory use and elapsed time.
- Capture the smallest required rectangle when a full page is unnecessary.
- Do not use a long fixed sleep as a substitute for a readiness signal.
- Measure on the same machine and network used by production jobs; PhantomJS behavior depends on its runtime and page resources.
Maintenance decision
PhantomJS’s suspended development matters when the target site depends on browser capabilities added after its QtWebKit-based engine. If the page is simple and your existing workflow is stable, the diagnostic sequence can resolve blank output. If compatibility failures continue, a maintained browser automation stack is the practical direction.
Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP or PDF. It removes cookie and consent banners, newsletter popups and chat widgets before capture; bot checks, blank pages, timeouts, failed loads and cache hits are not billed; response headers identify the page verdict and whether it was billed. Its MCP server provides take_screenshot, get_page_info and capture_pdf for Claude, Cursor and other MCP clients.
See the ScreenshotNeo API documentation for request options. This is the one-call version:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
There is a free plan with 1,000 screenshots per month and no card. Paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Why does page.open say success but the screenshot is blank?
Loading completed, but the application may still be rendering, the clip may exclude the content, or the page may have a transparent background. Check readiness, dimensions and alpha before treating it as a network failure.
Should I always add a five-second delay?
No. A fixed delay can confirm that timing is involved, but a selector or application state is more reliable and usually faster.
Can PhantomJS render every modern website?
No guarantee exists. Its development is suspended and its older engine may lack capabilities required by current sites.
Does a transparent PNG prove that the DOM is empty?
No. It can mean the page has no background color. Set an explicit background and inspect the alpha channel.
When should I remove clipRect?
Remove it whenever you are diagnosing a blank result. Reintroduce it only after an unclipped viewport capture works.


