ScreenshotNeo

BlogHow-to

How to Capture a Website Screenshot with PhantomJS After JavaScript Loads

Use PhantomJS to capture a page after it loads, then wait for the specific JavaScript-rendered content your screenshot needs.

By the ScreenshotNeo team4 October 20267 min read

Use PhantomJS’s webpage module to open the URL, check that loading succeeded, wait for the page-specific content you need, and then call page.render(). The page.open() callback is a useful load checkpoint, but it does not guarantee that every asynchronous widget, API request, or animation has finished. If your page fills in content after load, wait for a known DOM condition with a maximum timeout.

1. Basic PhantomJS screenshot

Save this as capture.js. Run it with the PhantomJS executable and provide the target URL and optional output filename:

var page = require('webpage').create();
var system = require('system');
var address = system.args[1];
var output = system.args[2] || 'capture.png';

if (!address) {
  console.log('Usage: phantomjs capture.js URL [output.png]');
  phantom.exit(2);
}

page.viewportSize = { width: 1280, height: 800 };

page.open(address, function (status) {
  if (status !== 'success') {
    console.log('Unable to load ' + address);
    phantom.exit(1);
    return;
  }

  page.render(output);
  console.log('Saved ' + output);
  phantom.exit(0);
});

For example: phantomjs capture.js https://example.com example.png. The viewport is set before opening the page so the page lays out at the intended dimensions. The output extension selects the render format.

2. Wait for JavaScript-rendered content

When the application fetches or computes important content after the load callback, poll for a meaningful page condition. This example waits until #results contains text, checks at 100 millisecond intervals, and exits with an error if the condition is not met within 15 seconds.

var page = require('webpage').create();
var system = require('system');
var address = system.args[1];
var output = system.args[2] || 'capture.png';
var maxWaitMs = 15000;
var pollMs = 100;
var startedAt;

if (!address) {
  console.log('Usage: phantomjs wait-for-content.js URL [output.png]');
  phantom.exit(2);
}

page.viewportSize = { width: 1280, height: 800 };

page.open(address, function (status) {
  if (status !== 'success') {
    console.log('Unable to load ' + address);
    phantom.exit(1);
    return;
  }

  startedAt = Date.now();
  waitForResults();
});

function waitForResults() {
  var ready = page.evaluate(function () {
    var element = document.querySelector('#results');
    return !!element && element.textContent.trim().length > 0;
  });

  if (ready) {
    page.render(output);
    console.log('Saved ' + output);
    phantom.exit(0);
    return;
  }

  if (Date.now() - startedAt >= maxWaitMs) {
    console.log('Timed out waiting for #results to contain text');
    phantom.exit(1);
    return;
  }

  setTimeout(waitForResults, pollMs);
}

Replace #results with a selector and condition that indicate the page is genuinely ready for your use case. A present element may still be empty, show a loading placeholder, or contain stale content; check the state that matters. page.evaluate() runs in the page context, so it can inspect DOM properties. The polling loop and timeout remain in the PhantomJS script.

Choose the readiness signal

Signal Use it when Limitation
Successful page.open() callback The required content is ready as part of ordinary page loading. It marks load completion, not completion of every later application update.
Short fixed delay The page has a known, fairly stable delay before a small update. A delay that is too short captures partial content; one that is too long wastes time. It is not proof that all network activity has settled.
Page-specific DOM condition Client-side rendering or an API response determines when the useful content appears. You must identify a reliable selector and state, and still enforce a maximum wait.

If a fixed delay is appropriate, schedule rendering after successful open rather than exiting in the callback:

page.open(address, function (status) {
  if (status !== 'success') {
    console.log('Unable to load ' + address);
    phantom.exit(1);
    return;
  }

  setTimeout(function () {
    page.render('capture.png');
    phantom.exit(0);
  }, 1500);
});

Choose that delay for the target page and keep an overall bound in production scripts. Neither a fixed sleep nor the load callback promises that every application-specific operation has completed.

3. Configure the capture

Viewport and crop

Set page.viewportSize before opening the URL. It controls the browser viewport and therefore affects responsive layout. To render only a region, set page.clipRect before page.render():

page.viewportSize = { width: 1280, height: 800 };
page.clipRect = { top: 0, left: 0, width: 900, height: 600 };

A clip rectangle selects a region; it does not make a viewport screenshot include the entire document. Decide whether you need the visible viewport or a specific crop, and verify that the chosen region contains the target content.

JavaScript, images, and resource timeout

In the documented webpage settings, JavaScript and image loading are enabled by default. You can configure settings such as a resource timeout in milliseconds before the initial page.open(). Settings apply during that initial call, so set them before opening the URL:

page.settings.resourceTimeout = 20000;
page.open(address, function (status) {
  // Handle status, wait for page-specific readiness, and render here.
});

A resource timeout bounds resource loading; it is not a signal that the page’s own JavaScript work is complete. Treat a timeout or failed resource as a condition to diagnose rather than proof of readiness.

Output formats and quality

page.render(filename) chooses a format from the filename extension. The documented API lists PDF, PNG, JPEG, BMP, PPM, and GIF where supported by the PhantomJS build. PNG is lossless; JPEG is lossy and can be smaller for photographic content. The render quality setting affects JPEG quality. For PNG, it controls lossless Deflate compression, so it can change file size without changing rendered pixels. Confirm that your build supports the format you select.

4. Handle failures and avoid premature exit

  • Check the page.open() status and return a nonzero exit code when it is not success.
  • On a readiness timeout, report which condition was missing and fail the job rather than silently saving a likely incomplete image.
  • Call phantom.exit() only after rendering or after all asynchronous work has completed. Exiting early ends the process before the screenshot is written.
  • If you load an external script with page.includeJs(), perform dependent work and exit from its callback so the process remains alive until the script finishes loading.

5. Troubleshooting

Symptom Likely cause What to check or change
Blank image The page failed to open, rendering happened before content appeared, or resources failed. Check the open status, wait for a populated page-specific selector, and inspect JavaScript and image settings and resource timeout.
Some content is missing The screenshot was taken at load completion while client-side work continued. Poll for the actual content or application state, and report a timeout if it never appears.
Process exits before the file is written phantom.exit() ran before asynchronous work or rendering completed. Move exit after page.render() and into the final asynchronous callback where applicable.
Output omits content below the fold The capture represents the viewport or a crop, rather than the full document. Check viewport and clip dimensions and whether your intended output is a viewport image or a region. A clip rectangle alone does not guarantee full-page capture.
Content is clipped or laid out unexpectedly The viewport dimensions or crop do not match the desired responsive layout. Set the viewport before opening and adjust the clip rectangle to cover the intended region.
Readiness wait always times out The selector is wrong, the element stays empty, or the target page never reaches the assumed state. Inspect the selector and test a condition tied to the real content. Do not treat element existence alone as success if it may be a placeholder.
Modern site renders incorrectly The page may depend on browser features or JavaScript behavior that this older WebKit-based workflow does not support. Validate PhantomJS against the actual target page. The documented API does not establish compatibility with any particular modern framework.

6. Performance, reliability, and cost

Keep the readiness check specific: short polling intervals make the condition responsive, while the maximum wait prevents a hung capture job. Avoid oversized viewports or unnecessary waits when they do not improve the required result. A DOM condition is generally a more useful completion rule than an arbitrary long sleep, but its reliability depends on choosing a state that truly represents finished content.

For repeatable runs, log the URL, load status, readiness outcome, elapsed wait, and output path. Treat open failures and readiness timeouts as explicit failures so downstream jobs do not accept blank or partial files. PhantomJS is a runtime-based workflow, so account for installing and maintaining the executable in the environment that runs the script. The research references provide no benchmark or general compatibility guarantee; measure against the actual pages and environment you need.

The script itself has no per-screenshot service charge, but you manage the runtime and its compatibility. If you need hosted capture, include network and operational setup in the comparison, and verify the target page’s behavior in the chosen workflow.

7. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns a PNG, JPEG, WebP, or PDF. Its [documentation](https://screenshotneo.com/docs/) describes the API; the product is at screenshotneo.com.

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

Python:

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)

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}`);

Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan. See the ScreenshotNeo docs and sign up for 1,000 free screenshots a month with no card.

FAQ

Does PhantomJS wait for every JavaScript operation automatically?

No. The open callback reports page load completion. Add a condition that reflects the asynchronous content your capture needs.

Should I use a fixed delay or poll the DOM?

Use a fixed delay only when the target has a known, stable delay. Prefer a page-specific DOM condition when content readiness can be observed, and cap either approach with a timeout.

Can I save a PDF instead of an image?

The render API lists PDF output as well as image formats, subject to support in the PhantomJS build. Use a PDF extension and confirm the selected build supports it.

Does PhantomJS guarantee modern framework compatibility?

No such guarantee is established by the documented API. Validate the exact page and scripts you intend to capture.