Learn

How image diffing works

Pixel comparison, colour distance, anti-aliasing detection, perceptual metrics, and what the two thresholds every tool exposes actually control.

Two images of the same size are compared pixel by pixel. For each position the comparator asks: are these two colours "the same"? Count the positions where the answer is no, draw them in red, and you have a diff image and a mismatch count. Everything a tool adds on top is about making that question less naive.

Colour distance

The first refinement is that rgb(120, 120, 120) and rgb(121, 120, 120) are not a difference anyone can see. Comparators compute a distance between the two colours and treat anything under a per-pixel threshold as equal. pixelmatch, the engine inside Playwright, Vitest and jest-image-snapshot, converts to the YIQ colour space, where luminance differences weigh more than hue differences, because that matches how eyes work. looks-same uses CIEDE2000, a perceptual colour difference formula. Resemble.js, behind BackstopJS, offers tolerances per channel.

The per-pixel threshold is the threshold option in pixelmatch-based tools (0 to 1, default 0.1) and the "ignore colors" or "ignore less" modes in Resemble. Raising it hides subtle colour shifts; lowering it makes anti-aliasing visible.

Anti-aliasing

Text and diagonal edges are drawn with partially transparent pixels along their border, and the exact values depend on the font engine, the GPU and the browser version. Two renders of the same text can differ in hundreds of pixels that no one can tell apart. pixelmatch detects likely anti-aliased pixels (a pixel that sits between two very different neighbours) and ignores them by default; odiff and looks-same do the same. This single feature is why pixel-level comparison works at all for text-heavy UIs.

The image-level threshold

After per-pixel decisions, the tool has a count of differing pixels. The second threshold says how many are allowed before the comparison fails: as an absolute number (maxDiffPixels in Playwright, failureThreshold in pixels for jest-image-snapshot) or as a share of all pixels (maxDiffPixelRatio, allowedMismatchedPixelRatio, errorThreshold, misMatchThreshold in percent for BackstopJS). The manifest records this share as comparison.diffRatio next to the threshold it was checked against.

A ratio is portable across viewports, a pixel count is not: 100 pixels is a lot in a 200×100 icon and nothing in a full page. Most teams end up with a small ratio (0.1% to 1%) and a handful of exceptions.

Dimension mismatch

If the actual image has a different size, there is nothing to compare. Tools either fail outright (Playwright, with a message about the expected size) or compare the overlapping region and report the difference (Resemble reports isSameDimensions and the delta). A size change usually means content moved below the fold or a scrollbar appeared; it is a real finding, not noise.

Perceptual comparison

Pixel comparison with anti-aliasing detection is good enough for most UIs. Where it is not, the alternatives are:

  • SSIM, the structural similarity index, compares local patterns of luminance, contrast and structure rather than single pixels. jest-image-snapshot offers it as comparisonMethod: 'ssim'; it is more tolerant of compression and sub-pixel shifts, and slower.
  • Hosted services (Applitools, Percy, Chromatic) run their own comparison on the server, often with layout-aware or AI-assisted modes that try to tell "the text moved" from "the text changed".
  • Ignore regions mask areas that are expected to change (a clock, an avatar, an ad slot). Playwright's mask option, odiff's ignore regions and the stylesheet trick (inject CSS that hides the element before the screenshot) all do this.

Reading a diff image

Most tools draw differing pixels in a highlight colour over a faded copy of the baseline. jest-image-snapshot and Roborazzi write a three-panel composite (baseline, diff, actual) in one file; Playwright writes three separate files and shows them with a slider in its report. When you build your own review step, a slider between baseline and actual is what reviewers reach for first; the diff image is for finding the one changed pixel in a sea of same.

Further reading

  • pixelmatch: 150 lines, readable in ten minutes, and the reference for how most tools decide.
  • odiff: the same ideas, compiled, for when pixelmatch is the slow part of your run.
  • Playwright's screenshot options, the clearest documentation of the two thresholds.