ScreenshotRender
← Back to blog
Tutorials

Visual Regression Testing with a Screenshot API (Step by Step)

Robert Belt·10 min read
Updated On :
Orange minimalist illustration of two stacked webpage screenshots with the pixel differences highlighted between them

A button moved twelve pixels to the left on Friday afternoon and nobody noticed until Monday, when a customer emailed asking where the checkout had gone. Unit tests passed, end to end tests passed, the deploy was green. By the end of this post you will have a three step visual regression loop (baseline, current, diff) that would have caught that button on the pull request, with one HTTP call doing all the rendering.

Visual regression testing is the only test that compares what the user actually sees. The reason it has a reputation for being flaky is almost always the same: the test runs Chromium locally, the baseline was captured on a different machine, and two pixels of antialiasing on the letter g mark the whole page as changed. Move the rendering to a fixed environment and the flake goes away. That is the job of a screenshot API like ScreenshotRender: render every capture on the same infrastructure so the only thing that can change between two runs is the page itself.

What is visual regression testing and why does it keep breaking in CI?

Visual regression testing is the practice of taking a screenshot of a page, storing it as the baseline, and on every later run comparing a fresh screenshot against that baseline pixel by pixel. Any difference above a small tolerance fails the test. It catches CSS regressions, font swaps, image crops, and layout shifts that no markup level test can see.

The reason teams rip it out after a month is almost never the technique. It is the rendering environment. A baseline captured on macOS uses Apple system fonts; the CI runner on Ubuntu falls back to DejaVu Sans and every paragraph reflows. Chromium 131 ships a new emoji set and the baseline goes red on a page that has not changed in two years. Web fonts load 80 milliseconds later than they did yesterday and the first paint is captured before the swap. The HTTP Archive Web Almanac reports that over 80 percent of sites use web fonts, so this is the normal case, not the edge case.

The fix is to capture the baseline and the current screenshot on the same machine, with the same fonts, the same Chromium build, and the same viewport. A screenshot API does that for free because every request runs on its own infrastructure, not yours.

Why does headless Chrome give a different screenshot every run?

Headless Chrome is deterministic in theory and noisy in practice. The same URL produces a different PNG when fonts finish loading at different times, when an animation is mid frame at the moment of capture, when a video poster has not painted yet, or when an ad slot renders something different on each request. Run the same page.screenshot() ten times against a typical marketing page and you will get three or four byte different PNGs.

You can fight this locally. Freeze animations with prefers-reduced-motion (see the MDN reference), wait for networkidle, hide ad iframes with a custom stylesheet, pin the Chromium version in your Dockerfile. It works. It also turns into a parallel codebase that breaks every time Chromium ships a release. Most teams write it once, debug it for a sprint, and quietly disable the test.

The alternative is to push the whole rendering layer to someone whose only job is to keep it stable. ScreenshotRender uses a real Chromium build and renders at a fixed 1280 by 720 viewport unless you pass fullPage=true for the full scrollable height. The cookie banners and ads that usually contaminate the baseline are removed automatically before the capture, so the PNG you get back is just the page, not the page plus a GDPR modal that moved three rows since yesterday.

Stop chasing phantom diffs. Get the same PNG every run.

Skip the Chromium version pinning, the font installs, and the EC2 fleet. One HTTP call returns a stable PNG with cookie banners and ads already removed, so your visual regression baseline stays the baseline.

Try a render

How do you capture a stable baseline screenshot of a webpage?

Capture the baseline with a single GET request to the screenshot API, save the returned PNG to your repo (or to a release artifact), and commit it. The full request fits on one line: https://screenshotrender.com/api/v1/screenshot?apiKey=YOUR_API_KEY&url=https://en.wikipedia.org/wiki/HTTP&fullPage=true. The response is JSON with a data.screenshot field pointing at the hosted PNG, which you download once and store asbaseline.png.

Three things make this baseline durable across months of CI runs:

  • Fixed viewport. The API renders at 1280 by 720 by default, so the layout does not shift between captures even if a junior developer reruns the test from a 4K monitor.
  • Cookie banner removal by default. The single biggest source of false positives on real sites is the cookie modal that appears on the first visit and not on the second. The API hides it before the screenshot.
  • Edge cache on identical requests. Rerunning the same URL with the same parameters returns the cached PNG from the CDN, which is exactly what a stable baseline needs (and it does not count against your monthly quota).

If the page under test is behind Cloudflare, the baseline itself will be a challenge page unless stealth is on. See how to screenshot a Cloudflare protected website for the parameter set. For long pages with content below the fold, full page capture ensures the diff covers everything, not just the first viewport.

How do you diff two screenshots and flag the difference?

Use pixelmatch, a small open source library from Mapbox that compares two PNG buffers and returns the number of different pixels. The script is about twenty lines: read baseline.png, fetch the current screenshot from the API, pass both buffers topixelmatch with a threshold, and exit with a non zero code if the diff count is over your tolerance. The library also writes a diff.png that paints the changed pixels in red so a human reviewer can see exactly what moved.

Two defaults that work for most apps. Set the per pixel threshold (how aggressively to ignore antialiasing) to 0.1; that is the pixelmatch default and it handles font rendering noise without masking real changes. Set the total tolerance (what fraction of pixels must differ to fail the test) to 0.1 percent of the image area. On a 1280 by 720 capture that is about 920 pixels, which is roughly one rendered character.

For a richer reviewer experience without writing the diff loop yourself, drop in Resemble.js for analysis output (percentage difference, bounding box of the change) or wire up BackstopJS for the full capture, diff, and approval workflow.

When does pixel diffing fail and what should you use instead?

Pixel diffing fails on pages that are supposed to change. A live stock ticker, a personalized hero with the visitor name, a carousel that auto rotates, a date stamp in the footer; all of them break a naive baseline on every run. The fix is not to abandon visual regression. The fix is to teach it where to look.

  • Mask the dynamic regions. Most diff libraries accept an ignore rectangle. Cover the ticker, the carousel, and the timestamp; the rest of the page still gets pixel level coverage.
  • Test components, not full pages. If a marketing page is too volatile, point the screenshot API at a static component playground URL (Storybook, Ladle) and diff the components in isolation.
  • Switch to a perceptual diff. For pages with gradients or photographic content, pixelmatch will flag JPEG recompression noise. Tools like Resemble.js withignoreColors reduce that to zero.

And there is one case where you should drop pixel diffing entirely: when the only thing you actually care about is whether a specific element is on the page. That is a DOM assertion, not a screenshot test. Use Playwright or Cypress for it. The screenshot test exists to catch the things the DOM assertion would miss.

Common questions about visual regression testing

Is visual regression testing the same as snapshot testing?

No. Snapshot testing in Jest or Vitest serializes a component tree to a text file and compares strings. Visual regression testing renders the page in a real browser and compares the resulting PNG pixel by pixel. Snapshot tests catch markup changes, visual regression tests catch CSS, font, image, and layout changes that the markup will not surface. Most production apps run both for different reasons.

How much pixel difference should fail the test?

A tolerance of 0.1 percent of total pixels is a common default for full page captures and rejects almost no real change while absorbing antialiasing noise on text edges. If your pages render heavy gradients, charts, or maps, raise it to 0.5 percent and call it good. The number that matters is the one that produces zero false positives on a known good baseline over a hundred runs. Tune the threshold until that holds, then leave it alone.

Can I run visual regression tests in GitHub Actions?

Yes, and it is the cleanest place for them. Call the screenshot API from a workflow step, store the baseline PNG in the repo or in a release artifact, and have the job exit with a non zero code when pixelmatch reports too many different pixels. Because a screenshot API runs Chromium on its own infrastructure, you avoid installing browsers in the runner and the workflow finishes in seconds instead of minutes.

What is the best tool for visual regression testing?

It depends on how much you want to own. BackstopJS is the most batteries included open source option and bundles capture and diff. Percy and Chromatic are hosted services that handle review queues and approvals. The DIY route in this article (screenshot API plus pixelmatch) is the lightest, costs nothing extra if you already have a screenshot API, and stays inside your repo. Pick the lightest tool that solves your specific problem.

Do I need a screenshot API or can I use Puppeteer directly?

Puppeteer works for a few pages on one machine, but the moment your CI runner upgrades Chromium, installs a different font, or runs on a different CPU, the baseline drifts and every test goes red. A screenshot API renders on fixed infrastructure, so the byte output for the same URL stays stable across runs. If you only need one or two baselines, Puppeteer is fine. If the test runs in CI on every pull request, the API is worth it just to stop chasing phantom diffs. The free tier on the ScreenshotRender playground is 100 screenshots per month with no credit card, which is enough to wire visual regression into a small project end to end before paying for more volume.

Keep reading