Pixel Diff vs Perceptual Diff: Definition and Data

A pixel diff counts the pixels that changed between two screenshots. How it differs from a perceptual diff, measured on four controlled changes, with code.

By Simon Chiu · Updated

A pixel diff compares two images position by position and reports the pixels whose colour differs by more than a threshold; a perceptual diff instead scores how different the images look to a person, usually by comparing local structure, brightness and contrast.

Both answer "did this screenshot change?". They disagree on what counts as a change, and the disagreement decides whether a visual test is useful or noisy. This entry is part of the screenshot glossary.

The difference in plain terms

A pixel diff is literal. It walks both images, compares the colour at (0,0), then (1,0), and so on, and counts the mismatches. It can also paint them red in a third image, which is the picture most people know from test reports.

A perceptual diff is statistical. A common one, SSIM (structural similarity), splits the image into small windows and compares mean brightness, contrast and the pattern inside each window. The result is a score from about 0 to 1, where 1 means the same structure.

The choice matters most in visual regression testing, where the diff is the pass or fail signal.

What each method reports: four controlled changes

We captured example.com at 1200×630, then captured it again four ways, and compared each with the first capture. Every capture pinned prefers-color-scheme: light, because on our test machine headless Chrome otherwise followed the dark system theme.

Change Pixels changed, threshold 0 Pixels changed, threshold 0.1 Share at 0.1 SSIM
None (a second capture) 0 0 0.00% 1.0000
Whole page moved 1 px right 21,705 16,776 2.22% 0.9362
Text colour #000000 to #010101 9,516 0 0.00% 1.0000
One word replaced in the paragraph 5,971 5,149 0.68% 0.9756

Method: Puppeteer 25.14.0 with headless Chrome 155 on macOS (Apple M1 Pro), 2026-10-15. pixelmatch 8 with default options apart from the threshold. SSIM is the mean over 8×8 luma windows, computed by the script that produced this table. Each row is one capture; the identical row confirms the page renders the same way twice.

Three things stand out:

  • Threshold 0 is noise. A colour change of one step out of 255 flagged 9,516 pixels. At 0.1 it flagged none.
  • A shift beats a content change. Moving everything by one pixel produced three times more changed pixels than a reworded sentence. SSIM agreed: it scored the shift as the bigger change.
  • Neither method knows what matters. The word change is the real regression, and both methods ranked it last.
pixelmatch diff for the 1 px shift: the outline of every line of text on example.com, in each of its languages, is marked in red, although nothing on the page changed except its position pixelmatch diff for the one-word change: only the first paragraph is marked red, because the new word re-wrapped that paragraph; the rest of the page is grey and unchanged
pixelmatch output at threshold 0.1 for the 1 px shift (top, 16,776 pixels) and the one-word change (bottom, 5,149 pixels). Captured with Puppeteer 25.14.0 and headless Chrome 155 on 2026-10-15, scaled to 600×315 WebP.

How it works technically

pixelmatch does not compare raw RGB. It converts each pixel to the YIQ colour space and measures a weighted distance, so the threshold option (0 to 1) roughly follows perceived colour difference. By default it also detects anti-aliased pixels, the soft edges of text and shapes, and does not count them. That is why the counts at threshold 0.1 are lower than at 0.

SSIM compares windows, not pixels, so a one-step colour change inside a window barely moves its score. A shift, though, changes the pattern in every window that holds an edge, so SSIM drops. Perceptual hashes (dHash, pHash) go further: they shrink the image to a few dozen pixels and compare a fingerprint. They are good for "is this the same page?" and too coarse for UI tests.

How to set it

Puppeteer with pixelmatch

Puppeteer has no diff of its own. Capture PNGs (lossless) and compare them:

import { readFile, writeFile } from 'node:fs/promises';
import pixelmatch from 'pixelmatch';
import { PNG } from 'pngjs';

const [a, b] = await Promise.all(['baseline.png', 'current.png'].map(async (f) => PNG.sync.read(await readFile(f))));
if (a.width !== b.width || a.height !== b.height) throw new Error('Image sizes differ; compare like with like');

const diff = new PNG({ width: a.width, height: a.height });
const changed = pixelmatch(a.data, b.data, diff.data, a.width, a.height, { threshold: 0.1 });
await writeFile('diff.png', PNG.sync.write(diff));

const ratio = changed / (a.width * a.height);
console.log(`${changed} pixels changed (${(ratio * 100).toFixed(2)}%)`);
if (ratio > 0.01) process.exitCode = 1; // fail the build above 1%

Playwright Test

Playwright Test has the comparison built in. threshold is the same per-pixel YIQ tolerance, and maxDiffPixelRatio is the share of pixels allowed to change:

import { test, expect } from '@playwright/test';

test('home page matches baseline', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveScreenshot('home.png', {
    threshold: 0.1,
    maxDiffPixelRatio: 0.01,
    animations: 'disabled',
  });
});

Tested on 2026-10-15 with Node 22.18.0, pixelmatch 8 and @playwright/test 1.64.1 (Chromium 156), with viewport 1200×630 and colorScheme: 'light' set in the config. The test passed against its own baseline and failed on the 1 px shift with "16670 pixels (ratio 0.03 of all image pixels) are different".

RenderScreenshot

The API does not diff images. It gives you the stable capture step: the same browser build, viewport and fonts for the baseline and for every later run. Request PNG (the default for the og_card preset) and send both images to the pixelmatch code above. Turn off the cache for the candidate capture, or you compare the baseline with itself:

curl -sS --fail -X POST https://api.renderscreenshot.com/v1/screenshot \
  -H "Authorization: Bearer $RS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com", "preset": "og_card", "browser": {"dark_mode": false, "reduced_motion": true}, "cache": {"enabled": false}}' \
  -o current.png

dark_mode: false pins the light theme and reduced_motion: true asks the page to skip its animations. Both remove sources of noise. The element screenshots guide shows how to capture one component instead of the whole page.

Common mistakes

  • Diffing JPEG or WebP. Lossy compression changes pixels on its own. Compare PNGs.
  • Comparing images of different sizes. pixelmatch throws. A taller page means the layout changed; report that directly.
  • Trusting the number alone. As the table shows, the count says how much moved, not whether it matters. Open the diff image.
  • Full-page diffs only. One shift near the top marks everything below it. Diff components and regions too.
  • Baselines from another machine. Different fonts or a different device pixel ratio fail every comparison.

For review and approval of baselines, read the visual regression testing guide. To get identical captures on every run, create a free account and use the request above.

Frequently asked questions

What threshold should a pixel diff use?
Start with a per-pixel colour threshold of about 0.1 (pixelmatch and Playwright both use a 0 to 1 scale) and a small allowed ratio of changed pixels. In our test, threshold 0 flagged 9,516 pixels for a colour change nobody can see, and threshold 0.1 flagged none.
Is a perceptual diff better than a pixel diff?
Neither ranks changes by importance. In our measurement, SSIM scored a one-pixel page shift as a bigger change than a reworded sentence. Use a pixel diff with a threshold as the gate, and look at the diff image to judge the change.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.