What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

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

Open the page once, collect each element’s bounding rectangle inside page.evaluate(), assign each rectangle to page.clipRect, and call page.render() with a different filename. The complete PhantomJS script below checks the load status, skips missing or zero-size elements, converts viewport coordinates to page coordinates, and exits only after all captures are written.

How the capture pipeline works

PhantomJS separates browser-context code from the outer script. The callback passed to page.open() runs in PhantomJS, where you control files, clipping, rendering, and process exit. The function passed to page.evaluate() runs inside the loaded page, where document, element IDs, styles, and layout information are available.

  1. Load the URL. Call page.open(address, callback) and stop if the callback status is not success.
  2. Measure targets in the page. Pass the ID array into page.evaluate(). Return plain objects containing serializable numbers and strings, not DOM nodes or functions.
  3. Convert coordinates. getBoundingClientRect() reports coordinates relative to the viewport. Add window.pageXOffset and window.pageYOffset to obtain page-relative coordinates for clipping.
  4. Render each region. Set page.clipRect to one rectangle, then call page.render() with a unique path. Repeat for every target.
  5. Exit deliberately. Call phantom.exit() after the loop, or after any asynchronous wait you add.

A single render call produces one image for the current clip rectangle. To save separate images, you must set a new rectangle and call page.render() for each element.

Requirements and important limitations

  • Install a PhantomJS build that can run the documented webpage API and execute the script from a command line.
  • Use IDs that identify the elements you actually want to capture. If an ID is absent, the script should report it instead of trying to read a rectangle from null.
  • Give the page a deliberate viewport. Responsive layouts can move or resize targets when the viewport changes.
  • The page.open() callback reports that the load operation succeeded or failed, but modern sites can insert or resize content after that callback. A delay or a bounded readiness check may be needed before measuring.
  • The supplied PhantomJS documentation does not establish current maintenance status or compatibility with present-day browsers, operating systems, or websites. Verify the exact PhantomJS version and target pages you intend to automate.

Complete PhantomJS script: one PNG per element ID

Save this as capture-ids.js. Replace address and the ids array, then run it with your PhantomJS executable. The script uses a short optional delay, rounds dimensions to whole pixels, and sanitizes IDs before using them in filenames.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
var page = require('webpage').create();

var address = 'https://example.com/';
var ids = ['header', 'main', 'footer'];
var WAIT_MS = 0; // Increase for content inserted after page.open().

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

function safeFilePart(value) {
  return String(value).replace(/[^a-z0-9_-]/gi, '_');
}

function captureElements() {
  var boxes = page.evaluate(function (elementIds) {
    return elementIds.map(function (id) {
      var element = document.getElementById(id);
      if (!element) {
        return { id: id, missing: true };
      }

      var rect = element.getBoundingClientRect();
      return {
        id: id,
        top: rect.top + window.pageYOffset,
        left: rect.left + window.pageXOffset,
        width: rect.width,
        height: rect.height
      };
    });
  }, ids);

  boxes.forEach(function (box) {
    if (box.missing || box.width <= 0 || box.height <= 0) {
      console.log('Skipping missing or empty element: ' + box.id);
      return;
    }

    page.clipRect = {
      top: Math.max(0, Math.round(box.top)),
      left: Math.max(0, Math.round(box.left)),
      width: Math.ceil(box.width),
      height: Math.ceil(box.height)
    };

    var filename = safeFilePart(box.id) + '.png';
    page.render(filename);
    console.log('Wrote ' + filename);
  });

  phantom.exit();
}

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

  if (WAIT_MS > 0) {
    window.setTimeout(captureElements, WAIT_MS);
  } else {
    captureElements();
  }
});

Run it from the directory where you want the files:

phantomjs capture-ids.js

The output names are based on the IDs, such as header.png, main.png, and footer.png. Characters that are awkward in filenames are replaced with underscores. The script keeps the page open while it renders and exits with status 1 when the initial load fails.

Choosing IDs, CSS selectors, or a combined capture

Known IDs

document.getElementById() is the simplest and fastest choice when the caller already has a list of IDs. It returns one element, so the input is naturally one screenshot target per ID.

CSS selectors

If callers provide selectors rather than IDs, keep the same outer loop but evaluate a selector in the page context. For one match, use document.querySelector(selector). For every match, use document.querySelectorAll(selector), copy each node’s rectangle into a plain object, and return that array. Pass the selector as an argument to page.evaluate(); do not build a closure that depends on outer PhantomJS variables.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Rank #2
Sale
var result = page.evaluate(function (selector) {
  var element = document.querySelector(selector);
  if (!element) {
    return { missing: true };
  }
  var rect = element.getBoundingClientRect();
  return {
    top: rect.top + window.pageYOffset,
    left: rect.left + window.pageXOffset,
    width: rect.width,
    height: rect.height
  };
}, '.pricing-card');

One larger image

For a single combined image, calculate a containing region and call page.render() once. Calling render() repeatedly with different rectangles creates separate files; it does not append regions to one image. A full-page render and a clipped render are different outputs, so choose the one that matches the downstream workflow.

Waiting for dynamically inserted content

Measure only after the target has reached the state you want to document. A fixed WAIT_MS is easy to add, but it is a timing guess: a fast page wastes time, while a slow API response may still be incomplete. A bounded polling function is more reliable when you know a specific element must appear.

function waitForId(id, timeoutMs, done) {
  var started = new Date().getTime();

  function poll() {
    var present = page.evaluate(function (elementId) {
      return !!document.getElementById(elementId);
    }, id);

    if (present) {
      done();
      return;
    }

    if (new Date().getTime() - started >= timeoutMs) {
      console.log('Timed out waiting for ' + id);
      done();
      return;
    }

    window.setTimeout(poll, 100);
  }

  poll();
}

// Inside the successful page.open callback:
waitForId('main', 10000, captureElements);

This checks presence, not visual stability. If scripts continue changing dimensions, wait for a page-specific condition, a known network completion signal, or a short settling delay before collecting rectangles. Nested frames require measuring inside the relevant frame and verifying how that PhantomJS build reports frame coordinates.

Coordinates, scrolling, and visual edge cases

  • Scrolling: getBoundingClientRect() is viewport-relative. Adding the page offsets, as the main script does, prevents a scrolled document from shifting the clip region toward the wrong location.
  • Zero dimensions: Hidden elements, collapsed containers, and elements with no rendered box have a width or height of zero. Skipping them avoids empty files.
  • Transforms: CSS transforms can make the visual bounds differ from an intuitive layout box. Test transformed targets on the exact PhantomJS build you deploy.
  • Fixed-position elements: Their rectangle is tied to the viewport. Confirm the result at the chosen viewport and scroll position.
  • Overflow: A child may extend outside an ancestor’s visible area. The clip rectangle follows the measured box; it does not automatically remove pixels hidden by overflow.
  • Responsive pages: Set page.viewportSize before opening the URL so media queries and measurements use the intended layout.

Output formats and file strategy

page.render() can write image formats documented by PhantomJS, including PNG and JPEG; the capture guide also lists GIF and PDF. PNG is a practical default for UI snapshots because it preserves sharp text and transparency where supported by the target build. Use a unique directory or filename prefix when running multiple URLs, otherwise two pages containing the same ID can overwrite each other.

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

For repeatable automation, record the URL, viewport, ID list, PhantomJS version, and timestamp beside the images. That metadata makes a visual difference easier to diagnose than a directory of anonymous files.

Or skip the browser setup

For a hosted screenshot API, ScreenshotNeo is the first option to try when you want clean shots, billing only for clean captures, and a paid plan that starts at $5. One GET request returns a PNG, JPEG, WebP, or PDF, so there is no PhantomJS process, viewport installation, or file-render loop to maintain.

With an API key, the same capture can be requested from cURL (the parameter reference is in the ScreenshotNeo documentation):

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
)
r.raise_for_status()
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}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo can accept consent banners before capture and remove more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and whether the request was billed. Its options include full-page capture with lazy images loaded, a CSS-selector element capture, dark mode, device presets or custom viewports, retina scale, PDF paper and page controls, custom CSS and JavaScript, clicks before capture, waits for selectors, delays or network idle, request and resource blocking, headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, configurable caching, signed image links, asynchronous jobs with signed webhooks, bulk capture for up to 100 URLs per call, a usage API, and an OpenAPI specification. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Plan Included screenshots Price
Free 1,000 per month $0, no card
Starter 3,000 $5
Growth 15,000 $15
Pro 60,000 $39
Scale 250,000 $99
Business 1,000,000 $249

Yearly billing gives two months free, and every feature is available on every plan. Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

PhantomJS troubleshooting

Symptom Likely cause Fix
Unable to load or a non-success status DNS, TLS, redirect, server, or timeout failure. Log the status, test the URL from the same machine, and stop before rendering. Do not treat a failed load as a valid screenshot.
Every target is reported missing The IDs differ from the loaded DOM, content is inside a frame, or scripts have not inserted it yet. Inspect the exact ID spelling, add a bounded wait, and measure in the correct frame context.
Images are blank or tiny The element is hidden, collapsed, or measured before layout settles. Check width and height, wait for content, and verify the viewport and responsive breakpoint.
The image is shifted after scrolling Viewport-relative rectangles were used as page coordinates. Add pageXOffset and pageYOffset, as in the complete script.
Files overwrite one another Repeated runs use identical filenames. Add a URL or run-specific prefix and write to separate directories.
Only the last region appears A single filename or clip rectangle was reused. Assign a distinct filename and call page.render() once for each rectangle.
Clips do not match modern layout Transforms, nested frames, or PhantomJS/WebKit differences. Test those pages on the deployed PhantomJS version and consider a current screenshot service when compatibility matters.

Performance, reliability, and cost decisions

One page load followed by several clipped renders is usually more efficient than reopening the URL for every ID. Keep the page open, collect all rectangles in one evaluation call, and render each region in memory before exiting. A very large target still consumes image memory, so split unusually tall regions or capture a full page only when required.

For reliable jobs, treat the initial status as a gate, use a finite readiness timeout, preserve logs, and make output names deterministic. Retrying a failed navigation can help with transient network errors, but do not silently replace a missing element with an empty image. If screenshots are part of a production pipeline, define what counts as success—loaded page, present target, non-zero dimensions, and written file—and report each condition separately.

PhantomJS itself does not require a paid screenshot service for this script; your operational costs are the machine, storage, network, and maintenance of the browser automation. A hosted API trades that setup for per-capture pricing and managed capture features. ScreenshotNeo’s free tier provides 1,000 screenshots each month without a card, while paid tiers begin at $5 for 3,000 screenshots.

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

Frequently asked questions

What happens if two elements have the same ID?

HTML IDs are intended to be unique. getElementById() returns one matching element, so duplicate IDs make the selected target dependent on the page’s DOM behavior. Correct the markup or use a selector-based collection when multiple matches are intentional.

Can I capture an element that is inside an iframe?

Measure it in the iframe’s document rather than the top-level document, then verify the coordinate conversion on your PhantomJS version. Frame offsets and cross-origin restrictions can prevent the outer page from reading the inner DOM.

Why does a screenshot differ between runs?

Dynamic content, asynchronous layout, ads, fonts, animations, viewport changes, and network timing can all alter pixels. Fix the viewport, wait for a page-specific ready condition, and disable or stabilize changing content where your page permits it.

Frequently Asked Questions

What happens if two elements have the same ID?

HTML IDs should be unique. getElementById() selects one matching element; use corrected markup or a selector-based collection when multiple matches are intentional.

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

Can I capture an element inside an iframe?

Measure it in the iframe document and verify frame-coordinate behavior on your PhantomJS build; cross-origin frames may be inaccessible.

Why can screenshots differ between runs?

Asynchronous content, animations, fonts, ads, viewport changes, and network timing can change pixels. Stabilize the viewport and wait for a page-specific ready condition.

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.