DEV Community

Cover image for Phasma: I Brought PhantomJS Back from the Dead (and It Runs with Just `pip install`)
Mohammad Raziei
Mohammad Raziei

Posted on • Edited on

Phasma: I Brought PhantomJS Back from the Dead (and It Runs with Just `pip install`)

In 2018, the PhantomJS maintainer posted a short message on GitHub:

"I am stepping down as maintainer. Slimer.js is mostly unmaintained. It's time for us to move on."

Headless Chrome had arrived. Everyone moved on. PhantomJS was officially dead.

But here's the thing nobody talked about: Chrome still isn't a Python package.


The Problem: Browser Automation Should Be pip install

Python has a philosophy — batteries included. You pip install requests and you have HTTP. You pip install numpy and you have numerical computing. Everything arrives as a package, versioned, isolated, reproducible.

So why does browser automation still require you to leave Python's ecosystem entirely?

  • playwright install downloads a 300MB Chromium binary outside your virtualenv
  • Puppeteer requires Node.js and npm
  • Selenium requires a separately installed browser and a matching ChromeDriver

These tools are powerful — but they're not Pythonic. They break the contract: one environment, one install, everything works.

I wanted something different. I wanted to write:

pip install phasma
Enter fullscreen mode Exit fullscreen mode

And have a fully working headless browser. No apt. No npm. No system packages. No setup steps outside of pip. Just Python.

That's phasma — and PhantomJS, it turns out, was the perfect engine to build it on. A self-contained binary with a full WebKit engine that ships inside the wheel. The ecosystem had abandoned it, but the binary never stopped working.


What Is Phasma?

Phasma is a Python package that wraps PhantomJS with a modern, Playwright-like async API. The PhantomJS binary ships inside the wheel — no system packages, no Node.js, no Chromium, nothing. Just:

pip install phasma
Enter fullscreen mode Exit fullscreen mode

And it works. On Linux, macOS, Windows. In Docker containers with no internet access. On HPC clusters. On that one server where the sysadmin says no.

A note on the name: In Star Wars, Captain Phasma is a chrome-armored stormtrooper who everyone thought was dead after The Force Awakens — and then came back in The Last Jedi. It felt like the right name for a package that resurrects a browser engine everyone declared dead. Also, PhantomJS is literally a phantom. It was right there.


The Real Use Case: Running DOM-dependent JavaScript Anywhere

Here's the concrete problem I kept running into. You have a JavaScript library that needs a real DOM to work. Maybe it's a charting library, a diagram renderer, a scraper, or just some legacy code that uses document.querySelector.

You want to run it from Python. Your options:

A real example from my own work: I needed to run Mermaid.js — the diagram-as-code library — from Python to generate SVG/PNG diagrams. Mermaid.js uses the DOM heavily. The official CLI (mmdc) requires Node.js. So I built mmdc for Python on top of phasma:

import asyncio
from phasma.svg import SvgRenderer

MERMAID_DIAGRAM = """
graph TD
    A[pip install phasma] --> B[Import in Python]
    B --> C[Launch Browser]
    C --> D[Run any JS with DOM]
    D --> E[Get results back]
"""

async def render_mermaid(diagram_code: str) -> bytes:
    # Inject Mermaid.js from CDN, render the diagram, export as PNG
    async with phasma.Browser() as browser:
        page = await browser.new_page()
        html = f"""
        <html>
        <body>
            <div class="mermaid">{diagram_code}</div>
            <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
            <script>mermaid.initialize({{ startOnLoad: true }});</script>
        </body>
        </html>"""
        await page.goto(html)
        await page.wait_for_selector(".mermaid svg", timeout=5000)
        svg = await page.inner_html(".mermaid")

    async with SvgRenderer() as r:
        return await r.to_png(svg)
Enter fullscreen mode Exit fullscreen mode

No Node.js. No npm install mermaid. Just Python.


How It Works Under the Hood

The old approach (every existing PhantomJS wrapper) looked like this:

Phasma works differently. When you call launch(), it starts one persistent PhantomJS process that runs a tiny HTTP server:

The result: ~9ms per operation instead of seconds. The same pattern Selenium WebDriver has used for years — I just applied it to PhantomJS.


The API

Phasma's API is deliberately close to Playwright so there's almost no learning curve:

import asyncio
import phasma

async def main():
    browser = await phasma.launch()
    try:
        page = await browser.new_page()

        # navigate
        await page.goto("https://example.com")

        # extract data
        title = await page.evaluate("document.title")
        heading = await page.text_content("h1")
        links = await page.evaluate("""
            Array.from(document.querySelectorAll('a'))
                 .map(a => ({ text: a.textContent, href: a.href }))
        """)

        # interact
        await page.fill("#search", "phasma")
        await page.click("#submit")
        await page.wait_for_selector(".results")

        # output
        await page.screenshot("shot.png")
        await page.pdf("page.pdf")

    finally:
        await browser.close()

asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

SVG Rendering — Also Batteries Included

One thing I needed constantly: converting SVG files to PNG/PDF without installing Inkscape or cairosvg. Phasma handles this too, with a dedicated SvgRenderer class:

from phasma.svg import SvgRenderer

async with SvgRenderer() as r:
    # basic conversion
    png = await r.to_png("diagram.svg")
    jpg = await r.to_jpeg("<svg ...>...</svg>")
    pdf = await r.to_pdf("chart.svg")

    # scale up for high-resolution export
    png_2x = await r.to_png("icon.svg", scale=2.0)   # 2× resolution
    png_4x = await r.to_png("icon.svg", scale=4.0)   # 4× resolution

    # PDF that fits the SVG exactly — no A4 borders
    pdf = await r.to_pdf("diagram.svg")              # paper = SVG dimensions

    # or standard paper size
    pdf = await r.to_pdf("diagram.svg", pdf_format="A4")
Enter fullscreen mode Exit fullscreen mode

One SvgRenderer = one PhantomJS process reused for all conversions. Batch-convert 100 SVGs and PhantomJS starts exactly once.


Running Any DOM-dependent JS: The General Pattern

This is the core superpower. Any JavaScript that needs a browser environment:

import asyncio
import phasma

# Your JS library that needs window, document, etc.
YOUR_LIBRARY_JS = """
    // imagine this is Chart.js, D3, Mermaid, or any DOM library
    var result = someLibraryThatNeedsDOM.process(inputData);
    result  // last expression is returned
"""

async def run_js_with_dom(js_code: str, html_context: str = "<html><body></body></html>"):
    browser = await phasma.launch()
    try:
        page = await browser.new_page()

        # set up the HTML context your JS needs
        import tempfile
        from pathlib import Path
        with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as f:
            f.write(html_context)
            await page.goto(Path(f.name).as_uri())

        # inject and run your library
        return await page.evaluate(js_code)
    finally:
        await browser.close()
Enter fullscreen mode Exit fullscreen mode

No Node.js. No Chrome. No system dependencies. Just Python and a pip install.


Benchmark

xychart-beta horizontal
    title "Time per operation (ms) — lower is better"
    x-axis ["goto()", "10× ops", "100× ops"]
    y-axis "milliseconds" 0 --> 3000
    bar [2500, 25000, 250000]
    bar [9, 90, 900]
Enter fullscreen mode Exit fullscreen mode

🔵 Old approach (per-process) — 🟣 Phasma (persistent session)

The difference is entirely startup cost. With phasma, PhantomJS starts once and stays alive.


Installation & Quick Start

pip install phasma
Enter fullscreen mode Exit fullscreen mode

That's it. The PhantomJS binary is bundled in the wheel.

import asyncio
import phasma

async def main():
    browser = await phasma.launch()
    page = await browser.new_page()
    await page.goto("https://example.com")
    print(await page.evaluate("document.title"))
    await browser.close()

asyncio.run(main())
Enter fullscreen mode Exit fullscreen mode

Who Is This For?

  • You're on a server where apt / yum is locked
  • You're building a minimal Docker image and don't want to pull in Chrome (~300MB)
  • You need to run DOM-dependent JavaScript from Python without Node.js
  • You want to convert SVG files without system dependencies
  • You need a headless browser that arrives via pip and nothing else

Playwright and Puppeteer are better tools for modern web automation — if you can install them. Phasma exists for the cases where you can't.


Links


Top comments (0)