ScreenshotNeo

BlogComparisons

Puppeteer Screenshot Comparison: pixelmatch vs. Resemble.js

Compare Puppeteer screenshots with pixelmatch or Resemble.js. See runnable examples, configuration options, and how to choose the right fit for your visual tests.

By the ScreenshotNeo team4 October 202610 min read

For Puppeteer visual regression tests, choose pixelmatch when you already decode baseline and candidate screenshots into equal-size pixel buffers and want a mismatch count or diff image. Choose Resemble.js when its built-in scaling, ignored regions, antialiasing controls, or diff styles solve a concrete need. Neither library is established as universally faster or more accurate; validate the settings against your own stable pages and CI environment.

Both libraries compare images captured by Puppeteer. The capture conditions matter: use the same viewport, browser environment, page state, and screenshot options for the baseline and candidate. Otherwise, the diff may reflect capture variation rather than a code change.

1. How the comparison pipeline works

A visual regression check has three parts: render the page, capture an image, and compare that image with a baseline. Puppeteer’s Page.screenshot() returns screenshot data; its options determine whether you capture the viewport, the full page, a clipped region, and which image type to produce.

  1. Load the page and wait for the application state your test intends to inspect.
  2. Capture the baseline or candidate with consistent Puppeteer options.
  3. Decode both images if your comparison library requires pixel arrays.
  4. Compare the images and fail or report the test according to your team’s mismatch policy.
  5. Save and inspect a diff image when a test reports a change.

Puppeteer documents Page.screenshot() and the available ScreenshotOptions. PNG is the documented default output type. Use PNG for comparison fixtures so lossy encoding does not introduce compression changes.

Capture choices to make explicit

Choice What it controls Practical guidance
Viewport or full page Visible viewport versus the entire document (fullPage) Pick one policy per test. Full-page captures can include content that only appears after scrolling or lazy loading.
Clip A rectangle of the page to capture Useful for a stable region or component, but keep the same coordinates and dimensions for both images.
Type PNG, JPEG, or WebP output Prefer PNG for pixel comparison; use the same type for both captures.
Omit background Whether to make the screenshot background transparent Set it consistently. Transparency changes can affect decoded pixels.

Waiting until the page reaches a repeatable state is application-specific. Disable or stabilize clocks, rotating content, random values, animations, and external data when those are not the subject of the test. This consistency advice follows from Puppeteer’s capture options and the image libraries’ input requirements; it is not a result of a comparative experiment.

2. pixelmatch: direct pixel data and mismatch count

pixelmatch compares raw image data. Its inputs must have equal dimensions. It accepts Buffer, Uint8Array, or Uint8ClampedArray data, takes explicit width and height values, returns a differing-pixel count, and can write an optional diff image.

Runnable Puppeteer and pixelmatch example

Install Puppeteer, pixelmatch, and PNGJS in a Node project. This example captures the current page, reads a baseline PNG, compares them, writes a diff PNG, and exits unsuccessfully if the mismatch ratio exceeds the chosen limit.

npm install puppeteer pixelmatch pngjs
// compare.mjs
import fs from 'node:fs';
import puppeteer from 'puppeteer';
import pixelmatch from 'pixelmatch';
import { PNG } from 'pngjs';

const url = process.argv[2] ?? 'https://example.com';
const baselinePath = 'baseline.png';
const candidatePath = 'candidate.png';
const diffPath = 'diff.png';
const maxMismatchRatio = 0.001; // Project-specific policy: 0.1%

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
  await page.goto(url, { waitUntil: 'networkidle0' });
  await page.screenshot({ path: candidatePath, type: 'png' });

  const baseline = PNG.sync.read(fs.readFileSync(baselinePath));
  const candidate = PNG.sync.read(fs.readFileSync(candidatePath));
  if (baseline.width !== candidate.width || baseline.height !== candidate.height) {
    throw new Error(`Dimension mismatch: baseline ${baseline.width}x${baseline.height}, candidate ${candidate.width}x${candidate.height}`);
  }

  const diff = new PNG({ width: baseline.width, height: baseline.height });
  const mismatched = pixelmatch(
    baseline.data,
    candidate.data,
    diff.data,
    baseline.width,
    baseline.height,
    { threshold: 0.1 }
  );
  fs.writeFileSync(diffPath, PNG.sync.write(diff));
  const ratio = mismatched / (baseline.width * baseline.height);
  console.log(`${mismatched} mismatched pixels (${(ratio * 100).toFixed(4)}%); diff: ${diffPath}`);
  if (ratio > maxMismatchRatio) process.exitCode = 1;
} finally {
  await browser.close();
}

Set baseline.png to a fixture captured from the expected page state. The threshold and allowed mismatch ratio above are example policy values, not universal recommendations or measured accuracy settings.

pixelmatch options that affect interpretation

Option Documented behavior When to consider it
threshold Ranges from 0 to 1; documented default is 0.1. Smaller values increase sensitivity. Tune against representative fixtures. Lowering it can surface subtle differences and more rendering noise.
includeAA Controls whether anti-aliased pixels are included; false is the documented default. Change only when your test policy explicitly wants those edge pixels counted.
windowSize When configured, the returned count becomes the highest mismatch count in any sliding N-by-N square, rather than a whole-image total. Can help assess localized changes separately from scattered noise. Do not treat it as a substitute for reviewing the diff.
Diff output Optional output buffer; the README documents controls for colors, alpha, masks, and checkerboard presentation. Choose the visualization that makes failures easiest for the team to inspect.

pixelmatch does not document built-in resizing or ignored bounding regions in the inspected API. Prepare same-sized images yourself if your capture dimensions differ, and mask or crop dynamic regions in a deliberate preprocessing step if needed.

3. Resemble.js: comparison controls and ignored areas

Resemble.js exposes an image comparison API with documented options for scaling one image to another’s size, ignoring antialiasing, defining bounding boxes and ignored areas, and customizing diff output. Those controls can simplify workflows with known dynamic regions or differently sized inputs, but scaling changes the comparison geometry; confirm that it matches your test’s intent.

Runnable Node.js comparison example

Resemble.js’s Node path uses node-canvas. Install it and validate that its native dependency works in the same OS and runtime image used by CI.

npm install puppeteer resemblejs
// resemble-compare.mjs
import fs from 'node:fs';
import puppeteer from 'puppeteer';
import resemble from 'resemblejs';

const url = process.argv[2] ?? 'https://example.com';
const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
  await page.goto(url, { waitUntil: 'networkidle0' });
  await page.screenshot({ path: 'candidate.png', type: 'png' });

  const baseline = fs.readFileSync('baseline.png');
  const candidate = fs.readFileSync('candidate.png');
  const result = await resemble(baseline)
    .compareTo(candidate)
    .ignoreAntialiasing()
    .outputSettings({
      errorColor: { red: 255, green: 0, blue: 255 },
      errorType: 'movement',
      transparency: 0.3
    })
    .toBuffer();

  fs.writeFileSync('diff.png', result);
  console.log(`Wrote diff.png (${result.length} bytes)`);
} finally {
  await browser.close();
}

This example demonstrates the documented promise-based Node comparison and buffer output path. Resemble.js also documents browser usage. Consult its README for the exact option names and supported output modes in the version pinned by your project.

Resemble.js controls to evaluate

  • Scale to same size: useful where dimensions vary, but resampling can conceal or alter layout differences. Prefer matching capture sizes when dimensions are part of the assertion.
  • Ignore antialiasing: changes which edge pixels count as errors. Validate the effect on your own pages.
  • Bounding boxes and ignored areas: exclude regions such as timestamps or rotating ad slots when those areas are intentionally outside the test contract. Keep exclusions narrow to avoid hiding meaningful regressions.
  • Diff styling: configure error color, error type, transparency, and output diff to fit your review workflow.
  • Large images: the repository README describes skipping pixels by default when width or height exceeds 1200, configurable with largeImageThreshold. Confirm this behavior in your installed version before depending on it.

The project README notes that the Node.js path depends on prebuilt node-canvas, which may fail in some environments. It describes installing without optional dependencies for browser-only analysis and mentions alternatives for Node builds. Check the current README and package version for setup details.

4. pixelmatch vs. Resemble.js: which should you choose?

Need Better starting point Why
Small comparison step over decoded, equal-size images pixelmatch Direct typed-array input, mismatch count, optional same-size diff output.
Scaling one image to another’s dimensions Resemble.js Scaling is documented as a comparison option.
Exclude a known dynamic region through comparison settings Resemble.js Documents bounding boxes and ignored areas.
Control the visual style of the diff Either Both document output customization, with different option models.
Minimize dependency friction in Node pixelmatch The project describes no dependencies; Resemble.js’s Node route uses node-canvas, which may fail in some environments.

Start with pixelmatch if equal dimensions and a mismatch count cover the requirement. Try Resemble.js when a specific built-in control saves meaningful preprocessing or improves diff review. Pin the versions, add stable fixtures, and decide thresholds from reviewed diffs. This recommendation is conditional on documented capabilities; no benchmark or side-by-side run established a speed or accuracy winner.

5. Reproducibility, performance, reliability, and cost

Make screenshots reproducible

  • Pin Puppeteer and comparison package versions in the lockfile.
  • Use a fixed viewport, device scale factor, browser version, locale, timezone, and color scheme where relevant.
  • Wait for the page state you intend to assert; avoid timing-only waits when a stable selector or application-ready signal is available.
  • Disable animations or freeze time and random data if they create irrelevant variation.
  • Keep fonts, assets, network fixtures, and browser provisioning consistent between baseline creation and CI.
  • Use the same screenshot type and full-page, clip, and background settings on both sides.

These are reliability practices inferred from the capture and comparison inputs. Puppeteer installation also affects reproducibility: its installation docs say puppeteer downloads a compatible Chrome for Testing by default, while puppeteer-core does not download Chrome and is intended for remote-browser or self-managed-browser use. This changes browser provisioning, not comparison behavior.

Performance and resource use

Both approaches require image data to be captured and processed. Keep captures to the region and dimensions your assertion needs; full-page images increase the amount of image data to decode and compare. Resemble.js’s documented large-image behavior may skip pixels above its threshold, so inspect the pinned version’s semantics before relying on it. No comparative timing measurements are available here, so benchmark your own pages and CI workers if runtime matters.

Cost

The libraries are used in your own test environment; account for CI compute, browser installation, storage of baselines and diffs, and any hosted visual review system you choose. The research reviewed no named vendor’s current commercial terms, so this guide does not compare hosted service pricing.

6. Troubleshooting common failures

Symptom Likely cause Fix
pixelmatch throws or comparison cannot proceed Image dimensions differ, or decoded data does not correspond to the supplied width and height. Print both dimensions before comparison; capture consistently or deliberately resize/crop before pixelmatch.
Every run has a diff Unstable page state, differing viewport or browser, animation, external content, or capture option drift. Stabilize the test fixture and compare the capture configuration before changing sensitivity thresholds.
Diff is too sensitive around text edges Antialiasing differences are counted according to current settings. Review pixelmatch’s includeAA or Resemble.js’s antialiasing control, then revalidate against known real changes.
Resemble.js fails to install or load in CI The Node path relies on prebuilt node-canvas, which can fail in some environments. Use a compatible build environment, follow the project’s installation guidance, or use its browser-only path when appropriate.
Large-image result misses an expected difference The documented Resemble.js README behavior can skip pixels for images above its large-image threshold. Check the installed version and configure largeImageThreshold as needed; confirm with a fixture containing a known change.
Diff highlights a timestamp, ad, or user-specific content The region is dynamic but included in the assertion. Freeze or fixture the content, clip the comparison, or use a narrowly scoped ignored area. Avoid broad masks that could hide regressions.
Screenshot is blank or incomplete The page was captured before it reached the intended state, or navigation failed. Wait for the relevant selector or application-ready state; log navigation errors and capture diagnostics.
Candidate has unexpected size Viewport, full-page mode, device scale, or clip differs from the baseline. Record these settings with the baseline and assert dimensions before pixel comparison.

7. Or skip the browser setup

If you need a screenshot without installing and provisioning Puppeteer, ScreenshotNeo returns an image or PDF from one API request. It accepts cookie banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, with verdict and billing details in response headers. Its MCP server lets AI agents use screenshot, page-info, and PDF tools. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000.

See the ScreenshotNeo API docs for request options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Use the [free ScreenshotNeo signup](https://screenshotneo.com/account/sign-up/) to get 1,000 screenshots a month with no card.

8. FAQ

Can pixelmatch compare JPEG or WebP screenshots?

It compares decoded pixel data, so the format is a decoding step. PNG is the straightforward choice for stable regression fixtures because Puppeteer documents PNG as its default screenshot type.

Should I make the threshold as low as possible?

No universal threshold fits every page. Smaller pixelmatch thresholds increase sensitivity; choose one by reviewing diffs from stable fixtures and known changes.

Can I use Resemble.js in a browser?

Yes. Its README documents browser usage. The Node-specific dependency concern is the node-canvas path.

Does either tool tell me whether a change is a real bug?

No. They report image differences according to configured comparison rules. A developer or an additional review policy decides whether the difference is expected.