BackstopJS vs Playwright
This is the closest pairing on the list, because almost nothing structural separates them: both are free, both run headless browsers locally or in CI, both store approved images in your repository. The whole comparison turns on one axis — who pins the browser, and who is still shipping releases.
Pick Playwright if you already run Playwright tests, or if "green locally, red in CI" has cost you an afternoon: the browser binaries are versioned with the test runner, and 1.62.0 shipped on 24 July 2026. Stay on BackstopJS if your value is in the backstop.json scenario list and the scrubber report, you don't have a Playwright suite to attach to, and you're willing to pin the version and the container image and leave it alone.
Side by side
BackstopJS vs Playwright at a glance
| BackstopJS | Playwright | |
|---|---|---|
| Cost | Free | Free |
| Licence | MIT | Apache-2.0 |
| Latest release | 6.3.25 — 7 Sep 2024 | 1.62.0 — 24 Jul 2026 |
| Open issues | 576, largely unanswered | Triaged continuously |
| Browser engine | Headless Chrome only | Chromium, Firefox, WebKit |
| Browser pinning | Your problem — puppeteer ^22.1.0 + a Docker image you maintain | Binaries versioned with the runner |
| How tests are declared | Declarative backstop.json — no code for the common case | TypeScript/JavaScript test files |
| Baselines | Committed to backstop_data/bitmaps_reference/ | Committed next to the test as *-snapshots/ |
| Approval flow | backstop approve promotes the whole run | --update-snapshots, per test or per project |
| Diff review UI | HTML report with before/after scrubber | HTML report with expected/actual/diff + trace viewer |
| Noise controls | misMatchThreshold, hideSelectors, onReady scripts | maxDiffPixelRatio, mask, animations:'disabled', retry-until-stable |
| Hosted review / approvals | ✗ | ✗ — neither has one |
| Best for | A config-file suite you pin and forget | Teams with an existing Playwright suite |
Sources: BackstopJS repo · npm registry data · Playwright visual comparisons docs. Read 28 July 2026.
Where Playwright wins
The browser is pinned for you
This is the whole argument. A pixel differ is only as trustworthy as the sameness of the rendering environment, and BackstopJS leaves that entirely to you: it declares puppeteer ^22.1.0, which resolves to whatever 22.x npm serves you, and the project's answer to environment drift is a Docker path you maintain yourself. Playwright installs a specific browser build alongside a specific runner version, and the snapshot filename records the platform it was taken on. You still can't mix macOS-approved baselines with Linux CI runs — nobody solves that — but you stop getting silent Chromium bumps.
It's actually being worked on
BackstopJS last published 6.3.25 on 7 September 2024 and hasn't committed since; there are 576 open issues, including an October 2024 report (#1586) about deprecated dependencies that nobody has answered. Playwright published 1.62.0 on 24 July 2026. If a Chromium change breaks screenshot capture, one of these projects will ship a fix.
Three engines instead of one
BackstopJS is headless Chrome only — a deliberate simplification, and it means a WebKit-only layout bug will never turn your suite red. Playwright captures in Chromium, Firefox and WebKit from the same test file, using projects, so cross-engine coverage costs configuration rather than money. It is not a real Safari on real hardware, but it catches the class of bug that a Chrome-only suite structurally cannot.
Better built-in defences against flake
toHaveScreenshot() retries until the screenshot stabilises, freezes CSS animations with animations: 'disabled', masks selected elements, and takes maxDiffPixels/maxDiffPixelRatio thresholds. BackstopJS has equivalents for most of that — misMatchThreshold, hideSelectors, delay, onReady — but the retry-until-stable behaviour is the one that removes a whole category of timing flake without you writing anything.
Where BackstopJS still wins
The config file is the feature. A backstop.json with thirty scenarios, four viewports and a few hideSelectors is readable by someone who doesn't write tests — a designer or a content editor can add a URL. The Playwright equivalent is code: a test file, a loop over viewport projects, imports. If your visual suite is maintained by people who aren't in the codebase daily, that difference is real and it doesn't show up in a feature table.
The scrubber. Playwright's report shows you expected, actual and diff for a failing assertion, which is fine. BackstopJS gives you a drag-slider across the whole run, and for scanning twenty small changes quickly it's still better. Several teams keep BackstopJS purely for this.
Zero attachment cost. BackstopJS is a standalone CLI: npx backstopjs test, no test framework, no runner conventions, no TypeScript config. If you have no Playwright suite, adopting Playwright to get a differ means adopting a test framework. That's a reasonable thing to do — but it is not a smaller change than pinning BackstopJS and moving on.
Migrating BackstopJS → Playwright
The path most teams take:
1. Inventory first. Every scenario in backstop.json maps to a test with a page.goto() and a toHaveScreenshot(). viewports become Playwright projects. hideSelectors and removeSelectors become mask (or CSS injected via page.addStyleTag). misMatchThreshold becomes maxDiffPixelRatio. What has no clean equivalent: onBefore/onReady scripts written against Puppeteer's API — those get rewritten against Playwright's, and clickable-element interactions are the fiddly part.
2. Accept a fresh baseline. Don't try to import bitmaps_reference. Playwright names files from the test title, project and platform and captures with its own browser build, so the old bitmaps will differ by font-rendering pixels anyway. Generate a new set with --update-snapshots and review that first set properly — it is the only time you'll look at all of them.
3. Run both for one cycle. Keep the BackstopJS job green alongside the new one for a release or two, and reconcile any diff either suite catches alone. It costs double CI minutes for a couple of weeks, which is the cheapest insurance in this migration.
4. What does not move. Your approval history. BackstopJS's record of what was approved and when lives in git commits against bitmap files; Playwright's lives in git commits against different bitmap files. Neither has an audit trail beyond that, so nothing is lost — but nothing carries over either, and if you were relying on "when did this page last change visually?", check that question still has an answer after the cutover.
Common questions
BackstopJS vs Playwright — FAQ
Can I reuse my BackstopJS reference images as Playwright baselines?
No, not reliably. Playwright names snapshot files from the test title, project and platform, and it captures with its own pinned browser build, so a bitmap produced by BackstopJS under Puppeteer 22 will almost certainly differ by a few pixels of font rendering. The practical approach is to accept a fresh baseline: write the tests, run them once to generate snapshots, then eyeball that first set carefully before committing it.
Does Playwright replace the BackstopJS scrubber report?
Partly. Playwright's HTML report shows expected, actual and diff images side by side for a failed screenshot assertion, and the trace viewer gives you the surrounding context. What it does not give you is BackstopJS's drag-slider scrubber across a whole suite of scenarios, which some reviewers genuinely prefer for scanning many small changes at once.
Is Playwright's toHaveScreenshot flaky on dynamic content?
It has the same fundamental problem as any pixel differ, but more built-in tools to manage it: a maxDiffPixels and maxDiffPixelRatio threshold, a mask option to black out selected elements, animations: 'disabled' to freeze CSS animations, and auto-retry of the assertion until it stabilises. BackstopJS offers misMatchThreshold and hideSelectors for the same job; the difference is that Playwright's retry-until-stable behaviour removes a common class of timing flake without extra configuration.