PhantomJS Alternative
Most developers searching for a PhantomJS alternative did not choose PhantomJS themselves. It came with the codebase: baked into a CI pipeline, wired into a PDF generator, or buried in an old test suite. Then an OS upgrade broke the binary, or a security audit flagged it red, and now you need a working replacement. This page covers the three realistic options in 2026: Puppeteer, Playwright, and a managed screenshot API, with migration code and honest trade-offs.
What PhantomJS was and why it stopped
PhantomJS launched in 2010 as the first practical headless browser. Before it existed, taking a programmatic screenshot of a web page was painful. PhantomJS made it simple: one binary that could load pages, run JavaScript, and export screenshots or PDFs. It became the foundation for CasperJS, hundreds of testing setups, and more PDF pipelines than anyone can count.
Then Chrome 59 shipped headless mode in June 2017. A real, actively maintained browser engine could now run without a GUI. PhantomJS's maintainer, Vitaly Slobodin, saw the writing on the wall and stepped down the next year. His message was direct: headless Chrome is faster and more stable than PhantomJS. The project was suspended in March 2018, and the GitHub repository was archived shortly after.
That was over eight years ago. Since then the web picked up CSS Grid, Flexbox, modern JavaScript, single-page apps, web fonts, and a dozen other features that PhantomJS will never understand.
What actually breaks when you run PhantomJS today?
PhantomJS renders pages using a QtWebKit engine from roughly 2013. Anything built with modern tools looks wrong or shows up blank.
| Feature | PhantomJS | Modern Chromium |
|---|---|---|
| CSS Flexbox | Partial, buggy | Full support |
| CSS Grid | Not supported | Full support |
| CSS custom properties | Not supported | Full support |
| ES6+ JavaScript (arrow functions, async/await, Promises) | Not supported | Full support |
| React / Vue / Angular rendering | Fails or blank page | Full support |
| Web fonts and emoji | Unreliable | Full support |
| WebP / AVIF images | Not supported | Full support |
| Shadow DOM | Not supported | Full support |
| Security patches | None since 2016 | Regular updates |
There are also unpatched vulnerabilities. CVE-2019-17221 allows arbitrary file reads through crafted HTML. CVE-2020-7739 is an SSRF in the npm package. Neither will ever be fixed. If your security team runs dependency audits, PhantomJS won't pass.
Honestly, the rendering is just the start. PhantomJS binaries break on newer OS versions. The npm package downloads over HTTP, which means the binary itself is vulnerable to tampering during install. And 511 npm packages still depend on it, creating a chain of unmaintained dependencies that won't ever get patched.
Migration options: self-hosted vs managed API
Puppeteer is the most direct PhantomJS replacement. Same language (JavaScript), similar mental model (launch browser, open page, capture screenshot). Google built it specifically for headless Chrome automation, and it arrived almost exactly when PhantomJS stopped. Around 10 million weekly npm downloads in 2026. If you have PhantomJS scripts in a Node.js codebase, Puppeteer is the natural rewrite target.
Playwright came two years later from the same team that originally built Puppeteer (they moved to Microsoft). It adds multi-browser support (Chrome, Firefox, WebKit), better auto-waiting, and stronger test isolation. If you need cross-browser screenshots or are starting fresh, Playwright is worth considering. See the Puppeteer vs screenshot API and Playwright vs screenshot API comparisons for the full picture.
Both require managing Chromium on your servers. Docker images grow past a gigabyte, each browser instance consumes hundreds of megabytes of memory, and you build retry logic for crashes and timeouts. For teams that already run Node.js infrastructure and need full browser automation (click sequences, form fills, multi-step workflows), self-hosting makes sense.
Here is what often gets overlooked though: PhantomJS was language-agnostic. It was a standalone binary you could call from PHP, Ruby, Java, Python, or a shell script. Puppeteer and Playwright are Node.js libraries. If your stack is Laravel, Rails, Django, or Spring, adding Node.js as a dependency just for screenshots isn't exactly a small ask.
A managed screenshot API restores that language-agnostic simplicity. You make an HTTP request from any language and get back an image or PDF. No browser binary on your server, no Node.js runtime to maintain, no process management. ScreenshotRun runs modern Chromium in the cloud, handles scaling, and returns results through a REST endpoint. The HTML to image and URL to PDF features cover the two most common PhantomJS use cases directly.
For teams that used PhantomJS for PDF generation specifically, the wkhtmltopdf alternative guide covers that migration path in depth, since wkhtmltopdf shared the same QtWebKit engine.
Code migration and feature comparison
A typical PhantomJS script for capturing a screenshot:
var page = require('webpage').create();
page.viewportSize = { width: 1280, height: 800 };
page.settings.userAgent = 'Mozilla/5.0 ...';
page.open('https://example.com', function(status) {
if (status === 'success') {
window.setTimeout(function() {
page.render('/tmp/screenshot.png');
phantom.exit();
}, 3000);
} else {
phantom.exit(1);
}
});
That is around 15 lines, plus you need the PhantomJS binary installed, process management to restart it when it crashes, and timeout handling for pages that hang. I remember spending a weekend debugging why PhantomJS would silently produce empty PNGs on certain pages. Turned out the site had switched to Flexbox and PhantomJS just gave up rendering half the layout.
The same capture with a cURL call to ScreenshotRun:
curl "https://api.screenshotrun.com/v1/screenshots/capture?url=https://example.com&width=1280&height=800&delay=3000&format=png" \
-H "Authorization: Bearer YOUR_API_KEY" \
-o /tmp/screenshot.png
One line. Works from any language that can make HTTP requests. For Node.js:
const params = new URLSearchParams({
url: 'https://example.com',
width: '1280',
height: '800',
delay: '3000',
format: 'png'
});
const response = await fetch(
`https://api.screenshotrun.com/v1/screenshots/capture?${params}`,
{ headers: { 'Authorization': 'Bearer YOUR_API_KEY' } }
);
const buffer = Buffer.from(await response.arrayBuffer());
For Python (replacing a PhantomJS subprocess call):
import requests
response = requests.get(
'https://api.screenshotrun.com/v1/screenshots/capture',
params={
'url': 'https://example.com',
'width': '1280',
'height': '800',
'delay': '3000',
'format': 'png'
},
headers={'Authorization': 'Bearer YOUR_API_KEY'}
)
with open('screenshot.png', 'wb') as f:
f.write(response.content)
For PHP, Ruby, Java, and Go, the pattern is the same: HTTP GET with your parameters, save the response body. No PhantomJS binary, no Node.js runtime, no browser to manage.
Here is how the two compare across capabilities:
| Capability | PhantomJS | ScreenshotRun API |
|---|---|---|
| Rendering engine | QtWebKit (~2013) | Chromium (latest) |
| JavaScript support | ES5 only | Full ES2024+ |
| CSS Grid / Flexbox | None / partial | Full support |
| SPA rendering (React, Vue, Angular) | Fails | Full support |
| Output formats | PNG, JPEG, PDF | PNG, JPEG, WebP, AVIF, TIFF, PDF |
| Full page capture | Manual viewport resize | full_page=true |
| Dark mode | Not supported | dark_mode=true |
| Retina / HiDPI | Manual zoomFactor workaround |
retina=true |
| Ad and cookie blocking | Not built in | block_ads=true, block_cookies=true |
| Language support | Any (standalone binary) | Any (REST API) |
| Maintenance | Suspended since 2018 | Active, updated Chromium |
| Cost | Free (self-hosted) | Free tier (200/month), paid plans after |
When self-hosting is still the right call
A managed API is the simplest PhantomJS replacement for screenshot and PDF use cases, but it's not always the right one.
If you need full browser automation beyond screenshots, click sequences, form submissions, multi-page workflows, then Puppeteer or Playwright give you the control an API cannot. PhantomJS was a general-purpose headless browser, and if you used it that way, a general-purpose replacement makes more sense.
Air-gapped environments that cannot make outbound HTTP calls need a self-hosted solution. Same for organizations where sending URLs or HTML to an external service is a compliance issue.
If you run high volume on a tight budget, self-hosting Chromium is cheaper per screenshot once you are past a few thousand daily. You pay in engineering time instead of API fees, which may or may not be a better deal depending on your team.
For everyone else, especially teams on PHP, Ruby, Python, Java, or Go who used PhantomJS as a callable binary, the REST API approach keeps things simple. You replace one external dependency with an HTTP call, and your screenshots start rendering the modern web again. Try ScreenshotRun's free tier on a page that PhantomJS renders incorrectly and see the difference firsthand.
Vitalii Holben