PhantomJS Alternative: Migrate Old Screenshot Scripts

PhantomJS development stopped in 2018. Replace a PhantomJS screenshot or PDF script with Playwright or one API call, with an API mapping and tested code.

By Simon Chiu · Updated

PhantomJS is finished. Its maintainer stepped down in 2017, development was suspended in 2018, and the last release (2.1.1) shipped in early 2016 with a WebKit engine from before CSS grid and modern JavaScript. If a PhantomJS script still renders screenshots or PDFs somewhere in your stack, it is producing captures of a web that no longer exists, from a binary that gets no security fixes. You have two practical replacements: Playwright, if you want to keep running a browser yourself, or a screenshot API, if all the script does is produce images or PDFs. This page maps every common PhantomJS call to both and gives tested replacements.

Disclosure: I build RenderScreenshot, which is one of the two options here. For many teams the Playwright route is the right one, and the code for it is below too.

This page is part of our screenshot API comparison series. If you are also replacing other legacy tools, see the wkhtmltoimage alternative.

Verdict

Migrate to Playwright if Migrate to RenderScreenshot if
Your PhantomJS script logs in, clicks or scrapes values with page.evaluate Your script only opens a page and calls page.render
You must render pages only your server can reach The pages are public, or reachable with cookies or headers
You are ready to run and update Chromium on your servers You want no browser on your servers at all
You want the most control over each step You want presets, caching and retries handled for you

Either way, do not try to keep PhantomJS alive. Its old WebKit cannot lay out many current sites, and no amount of setTimeout fixes missing CSS and JavaScript features.

The script most teams still run

Most surviving PhantomJS code is a variant of the classic rasterize script: open a URL, wait a moment, render to PNG or PDF. Something like this:

// capture.js: legacy PhantomJS 2.1 script (ES5). Run with: phantomjs capture.js <url> <out.png|out.pdf>
// Shown for reference; PhantomJS is not maintained.
var page = require('webpage').create();
var system = require('system');
var url = system.args[1];
var out = system.args[2] || 'out.png';

page.viewportSize = { width: 1280, height: 800 };
page.clipRect = { top: 0, left: 0, width: 1280, height: 800 };
page.settings.userAgent = 'Mozilla/5.0 (capture-bot)';
page.settings.resourceTimeout = 10000;
page.customHeaders = { 'Accept-Language': 'en-US' };
page.paperSize = { format: 'A4', orientation: 'portrait', margin: '1cm' };

page.onResourceRequested = function (data, request) {
  if (/\.(mp4|webm)$/.test(data.url)) request.abort();
};

page.open(url, function (status) {
  if (status !== 'success') {
    console.log('load failed: ' + status);
    phantom.exit(1);
  }
  // Give scripts time to run, then render.
  window.setTimeout(function () {
    page.render(out);
    phantom.exit(0);
  }, 1000);
});

We did not run this script for this page: PhantomJS has no native build for Apple Silicon Macs, and installing it on a server today is exactly what this page argues against.

PhantomJS to Playwright to RenderScreenshot: API mapping

PhantomJS Playwright RenderScreenshot
require('webpage').create() chromium.launch() then browser.newContext() and context.newPage() Not needed
page.viewportSize = { width, height } newContext({ viewport: { width, height } }) viewport.width, viewport.height
page.zoomFactor = 2 newContext({ deviceScaleFactor: 2 }) viewport.scale: 2
page.clipRect = { top, left, width, height } page.screenshot({ clip: { x, y, width, height } }) capture.clip
page.render('out.png') page.screenshot({ path: 'out.png' }) The response body (PNG by default)
page.render('out.jpg', { quality: 80 }) page.screenshot({ type: 'jpeg', quality: 80 }) output.format: "jpeg", output.quality: 80
page.render('out.pdf') with page.paperSize page.pdf({ format: 'A4', margin }) output.format: "pdf", pdf.paper, pdf.margin, pdf.landscape
page.settings.userAgent newContext({ userAgent }) browser.user_agent
page.settings.resourceTimeout page.goto(url, { timeout }) wait.timeout (seconds, max 60)
page.customHeaders newContext({ extraHTTPHeaders }) network.headers
phantom.addCookie() context.addCookies([...]) network.cookies
page.settings.userName / password newContext({ httpCredentials }) network.auth with type: "basic"
page.onResourceRequested + request.abort() page.route(pattern, (r) => r.abort()) block.requests (glob) or block.resources (types)
page.injectJs() / page.includeJs() page.addInitScript() / page.addScriptTag() page.scripts (inline or { url })
page.evaluate(fn) returning data page.evaluate(fn) No equivalent (the API returns images and PDFs)
window.setTimeout(render, 1000) waitUntil: 'networkidle' or locator.waitFor() wait.until, wait.for_selector, wait.delay
page.open(url, cb) status 'fail' page.goto() throws or returns a non-OK response HTTP error with a typed code and retryable
system.args process.argv Not applicable

The RenderScreenshot names are documented in the parameter reference; PDF options are on the PDF parameters page.

Route 1: the same script in Playwright

// capture-playwright.mjs: drop-in replacement for a PhantomJS capture script.
// Usage: node capture-playwright.mjs https://example.com out.png   (or out.pdf)
import { chromium } from 'playwright';

const [, , url, out = 'out.png'] = process.argv;
if (!url) {
  console.error('usage: node capture-playwright.mjs <url> <out.png|out.pdf>');
  process.exit(2);
}

const browser = await chromium.launch();
try {
  const context = await browser.newContext({
    viewport: { width: 1280, height: 800 },     // page.viewportSize
    deviceScaleFactor: 1,                       // page.zoomFactor (for sharper output use 2)
    userAgent: process.env.CAPTURE_UA,          // page.settings.userAgent (undefined = default)
    extraHTTPHeaders: { 'Accept-Language': 'en-US' }, // page.customHeaders
    colorScheme: 'light',
  });
  const page = await context.newPage();

  // page.onResourceRequested + networkRequest.abort()
  await page.route(/\.(mp4|webm)$/, (route) => route.abort());

  // page.open(url, cb) + status check + the setTimeout you used to wait for JS
  const response = await page.goto(url, { waitUntil: 'networkidle', timeout: 30_000 });
  if (!response || !response.ok()) throw new Error(`load failed: HTTP ${response?.status()}`);

  if (out.endsWith('.pdf')) {
    await page.pdf({ path: out, format: 'A4', printBackground: true }); // page.paperSize + render()
  } else {
    // page.clipRect + page.render()
    await page.screenshot({ path: out, clip: { x: 0, y: 0, width: 1280, height: 800 } });
  }
  console.log(`saved ${out}`);
} finally {
  await browser.close();
}
npm i [email protected]
npx playwright install chromium     # on Linux servers: npx playwright install --with-deps chromium
node capture-playwright.mjs https://example.com out.png

Tested on 2026-10-15 with Node.js 22.18.0 and Playwright 1.64.1 on an Apple M1 Pro:

Run Target Result Wall time
PNG, 3 runs https://example.com 1280x800 PNG, 80,922 bytes 1.32 s, 2.20 s, 1.30 s
PDF https://example.com A4 PDF saved
Bad page https://example.com/nope-404 Error: load failed: HTTP 404, exit code 1 n/a
No arguments n/a Usage message, exit code 2 n/a

The wall times include starting Chromium, because each call launches a new browser like each phantomjs call did. If your old setup called PhantomJS once per URL from a shell loop, keep one browser open and loop inside the script instead; that removes most of the per-capture time.

Three changes trip people up in this migration:

  1. colorScheme matters now. PhantomJS had no dark mode. Modern Chromium follows the host's colour scheme unless you set one, and on a Mac in dark mode we saw example.com render dark with the Chrome CLI. Set colorScheme: 'light' as above to keep old and new output alike.
  2. Promises replace callbacks. PhantomJS scripts are ES5 with callbacks and phantom.exit(). In Node.js, use await, try/finally to close the browser, and a non-zero exit code on failure, as above.
  3. Replace fixed sleeps with conditions. The setTimeout(render, 1000) pattern existed because PhantomJS had nothing better. waitUntil: 'networkidle' or waiting for a specific element is faster on quick pages and more reliable on slow ones. Our Playwright screenshot tutorial covers the wait options in depth.

Route 2: no browser at all

If the script only renders, replace it with an API call that takes the same two arguments. The browser, its updates and its fonts move off your server.

Node.js

// rasterize-api.mjs: same arguments as the PhantomJS script, no browser on this machine.
// Usage: node rasterize-api.mjs <url> <out.png|out.pdf>
import { writeFile } from 'node:fs/promises';
import { Client, TakeOptions } from 'renderscreenshot';

const [, , url, out = 'out.png'] = process.argv;
if (!url) {
  console.error('usage: node rasterize-api.mjs <url> <out.png|out.pdf>');
  process.exit(2);
}

const client = new Client(process.env.RS_API_KEY);
const options = out.endsWith('.pdf')
  ? TakeOptions.url(url).format('pdf').pdfPaperSize('a4').pdfPrintBackground()
  : TakeOptions.url(url).width(1280).height(800).waitFor('networkidle').format('png');

try {
  await writeFile(out, await client.take(options));
  console.log(`saved ${out}`);
} catch (err) {
  console.error(`capture failed: ${err.code ?? ''} ${err.message}`);
  process.exitCode = 1;
}

Python

"""rasterize_api.py: PhantomJS rasterize replacement in Python. Usage: python rasterize_api.py <url> <out.png|out.pdf>"""
import os
import sys

import requests

if len(sys.argv) < 2:
    sys.exit("usage: python rasterize_api.py <url> <out.png|out.pdf>")
url = sys.argv[1]
out = sys.argv[2] if len(sys.argv) > 2 else "out.png"

if out.endswith(".pdf"):
    body = {"url": url, "output": {"format": "pdf"}, "pdf": {"paper": "a4", "background": True}}
else:
    body = {
        "url": url,
        "viewport": {"width": 1280, "height": 800},
        "wait": {"until": "networkidle"},
        "output": {"format": "png"},
    }

resp = requests.post(
    "https://api.renderscreenshot.com/v1/screenshot",
    headers={"Authorization": f"Bearer {os.environ['RS_API_KEY']}"},
    json=body,
    timeout=90,
)
if resp.status_code != 200:
    sys.exit(f"capture failed: HTTP {resp.status_code} {resp.text[:200]}")
with open(out, "wb") as f:
    f.write(resp.content)
print(f"saved {out}")

Ruby

# rasterize_api.rb: PhantomJS rasterize replacement in Ruby. Usage: ruby rasterize_api.rb <url> <out.png|out.pdf>
require 'renderscreenshot'

url = ARGV[0] or abort('usage: ruby rasterize_api.rb <url> <out.png|out.pdf>')
out = ARGV.fetch(1, 'out.png')

client = RenderScreenshot.client(ENV.fetch('RS_API_KEY'))
options =
  if out.end_with?('.pdf')
    RenderScreenshot::TakeOptions.url(url).format('pdf').pdf_paper_size('a4').pdf_print_background
  else
    RenderScreenshot::TakeOptions.url(url).width(1280).height(800).wait_for('networkidle').format('png')
  end

begin
  File.binwrite(out, client.take(options))
  puts "saved #{out}"
rescue RenderScreenshot::Error => e
  abort "capture failed: #{e.code} #{e.message}"
end

All three were tested on 2026-10-15 against a local mock of the RenderScreenshot API for PNG output, PDF output and the missing-argument case (Node.js 22.18.0 with renderscreenshot 1.1.0; Python 3.14 with requests 2.34.2; Ruby 4.0.7 with the renderscreenshot gem 1.0.0). They check the request each one sends; they do not measure our rendering. Because each script keeps the PhantomJS argument order, a shell loop or cron line that called phantomjs capture.js "$url" "$file" only needs its command changed.

What changes in the output

  • Pages render correctly. This is the point of the migration, but it means images will not match your old ones pixel for pixel. Re-baseline any visual tests after the switch.
  • Fonts. PhantomJS used the fonts on your server. Playwright on Linux does too, so install the font packages your pages need. The API renders with our font set; send page.styles with @font-face rules if you need a specific font.
  • PDF page breaks. Chromium supports more of the CSS paged-media rules (@page, break-inside) than PhantomJS did, so page breaks can move. Check long tables and headers after the switch.

Cost at 1,000, 10,000 and 100,000 captures a month

Playwright is free; its cost is a server with enough memory for Chromium and the time to keep it updated. RenderScreenshot plans, checked 2026-10-15 (see pricing):

Monthly captures Plan Monthly price
Up to 150 Free, no credit card $0
1,000 Starter (2,500 credits) $10
10,000 Growth (12,000 credits) $40
100,000 Above Scale (60,000 credits for $100): custom plan Quote

When Playwright is the better replacement

  • Your PhantomJS code does more than render: it fills forms, clicks, or returns values from page.evaluate.
  • The pages live on an internal network the API cannot reach.
  • You already run Node.js services and can add Chromium to the image.
  • Capture volume is high and steady enough that a dedicated server is cheaper than per-capture pricing.

The Playwright alternative and Puppeteer alternative pages cover the next decision: when to keep running those yourself and when to hand captures to an API.

Retire PhantomJS this week

  1. Find every call: search your repositories and crontabs for phantomjs and for .render(.
  2. For each script, decide with the verdict table: Playwright or API.
  3. Port it with the mapping table, keeping the same arguments so callers do not change.
  4. Run old and new side by side on ten real URLs and check the output.
  5. Remove the PhantomJS binary from your images and servers.

To try the API route, create a free key and run rasterize-api.mjs with the URLs your PhantomJS job uses today.

Frequently asked questions

Is PhantomJS still maintained?
No. Its maintainer stepped down in 2017 and the project announced it was suspending development in 2018. The last release, 2.1.1, dates from early 2016 and uses an old WebKit engine that cannot render many modern sites correctly.
What is the best replacement for PhantomJS?
For scripts you want to keep running yourself, Playwright or Puppeteer with headless Chromium. For scripts that only produce screenshots or PDFs, a screenshot API removes the local browser entirely.
Can I run my old PhantomJS script in Playwright without changes?
No. The APIs differ and PhantomJS scripts are ES5 with callbacks. The mapping table on this page translates each PhantomJS call to its Playwright and RenderScreenshot equivalent, so the rewrite is mechanical for capture scripts.
Why do my PhantomJS screenshots look broken on modern sites?
PhantomJS's WebKit predates CSS grid support, modern JavaScript syntax and many web APIs. Pages that use them fail to lay out or run scripts, and you get blank areas or an unstyled page.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.