Visual regression testing in CI without running a browser
A UI change ships, and a button quietly disappears on mobile. Visual regression testing catches that — but maintaining headless Chrome in CI is its own job. Here's how to do it with one API call instead.
The problem with DIY visual testing
The usual setup: install Playwright or Puppeteer in CI, launch Chromium, screenshot pages, diff them against committed baselines. It works, until it doesn't:
- Browser installs and version drift slow every pipeline and break randomly.
- Full-page captures are flaky — lazy-loaded images, web fonts, animations and timing all produce false diffs.
- Committed baseline images bloat the repo and churn on every intentional change.
- Cookie banners and ads create diffs that have nothing to do with your code.
The alternative: a visual diff API
Instead of capturing and comparing yourself, call a visual diff API: send a baseline URL (production) and a candidate URL (your preview/staging deploy), and get back how much changed. Metrics first — you only download an image when something actually moved.
curl --request POST \
--url https://screenshot-e-pdf-render.p.rapidapi.com/v1/diff \
--header 'X-RapidAPI-Key: YOUR_KEY' \
--header 'X-RapidAPI-Host: screenshot-e-pdf-render.p.rapidapi.com' \
--header 'Content-Type: application/json' \
--data '{
"baseline": { "url": "https://prod.example.com/pricing" },
"candidate": { "url": "https://preview.example.com/pricing" },
"options": { "diffOutput": "metrics", "fullPage": true,
"ignoreSelectors": [".cookie-banner", "[data-dynamic]"] }
}'
{ "changed": true, "difference_percentage": 2.41,
"different_pixels": 18960, "total_pixels": 786432, "fast_path": false }
Fail the pull request on regressions
Drop this into GitHub Actions. It fails the build when the page changed more than 1%:
name: Visual diff
on: [pull_request]
jobs:
diff:
runs-on: ubuntu-latest
steps:
- name: Compare production vs preview
env:
RAPIDAPI_KEY: ${{ secrets.RAPIDAPI_KEY }}
run: |
curl -s --request POST \
--url https://screenshot-e-pdf-render.p.rapidapi.com/v1/diff \
--header "X-RapidAPI-Key: $RAPIDAPI_KEY" \
--header "X-RapidAPI-Host: screenshot-e-pdf-render.p.rapidapi.com" \
--header 'Content-Type: application/json' \
--data '{"baseline":{"url":"https://prod.example.com"},"candidate":{"url":"https://preview.example.com"},"options":{"diffOutput":"metrics","fullPage":true}}' \
-o diff.json
PCT=$(jq -r '.difference_percentage' diff.json)
echo "Difference: ${PCT}%"
awk "BEGIN{exit !(${PCT} > 1.0)}" \
&& { echo '::error::Visual regression > 1%'; exit 1; } \
|| echo 'Within threshold.'
Killing false positives
- Ignore the noise.
ignoreSelectors(CSS) andignoreRegions(x/y/width/height) exclude banners, ads, timestamps and dynamic widgets. - Deterministic capture. Animations paused, fonts loaded and media frozen before the shot, so the diff measures real change.
- Threshold tuning. Start at 1% and adjust; anti-aliasing is excluded by default.
- Fast path. Byte-identical pages return
fast_path:trueat 0% instantly — no pixel work, no cost spike on green runs.
When DIY still wins
If you need component-level snapshots inside a test runner (Storybook, jsdom), a local tool is the right call. The API approach shines for page-level checks across environments — pre-deploy diffs, production monitoring and cross-device comparisons — without owning browser infrastructure.
Try it
RenderShot's /v1/diff is metrics-first with ignore regions, selectors and a SHA-256 fast path. Free tier, no card.