Zum Artikel springen

Messung Veröffentlicht Lesezeit 11 Min.

Miss das Grundrauschen, bevor du einen visuellen Diff-Schwellenwert setzt

Zwei Läufe desselben Builds, auf derselben Maschine, ohne jede Änderung dazwischen: Die Seiten-Screenshots waren sich über gar nichts uneinig, die Canvas-Aufnahmen über bis zu 81 Prozent ihrer Pixel. Eine dieser Zahlen hat einen Schwellenwert um das Zehnfache verschärft. Die andere hat ein Gate disqualifiziert.

Kurz gesagt

Lass die Suite gegen sich selbst laufen: Nimm denselben Build zweimal auf derselben Maschine auf, vergleiche Lauf A mit Lauf B und behandle jeden Unterschied, der dabei auftaucht, als reines Rauschen des Messinstruments. Ein Schwellenwert unterhalb dieser Zahl schlägt zufällig fehl; einer weit darüber beweist nichts. Hier hat das Verfahren den Seiten-Schwellenwert zehnmal strenger gemacht, von 0.001 auf 0.0001, und das Canvas-Gate ganz disqualifiziert — zwei identische Läufe waren sich über bis zu 81 Prozent der Pixel eines Canvas uneinig, mit einem SSIM-Sturz auf 0.018148, wo das geschätzte Gate 0.98 verlangt hatte. Die zweite Hälfte des Verfahrens zählt genauso viel wie die erste: Prüfe, ob das Instrument überhaupt auf das Motiv gerichtet war, denn sechzehn der neunzehn Screenshots hinter dieser gemessenen Null sind ein flaches Magenta-Rechteck.

Was du mitnimmst

  1. Nimm denselben Build zweimal auf, vergleiche ihn mit sich selbst und behandle jeden Unterschied als die Grenze, unterhalb derer kein Schwellenwert etwas bedeutet.
  2. Das Messen hat den Seiten-Schwellenwert dieses Repositorys zehnmal strenger statt lockerer gemacht — 0.001 auf 0.0001 —, weil sich das Instrument als bytegenau herausstellte.
  3. Ein Grundrauschen von null ist kein Beweis für ein gutes Instrument: Sechzehn der neunzehn Screenshots dahinter sind 1.296.000 Pixel einer einzigen Farbe.
  4. Wenn kein Schwellenwert über dem Grundrauschen liegt, markiere das Gate als blockiert, zeichne die Zahlen weiter auf und benenne die konkrete Änderung, die es entsperren würde.
  5. Drucke die Vergleiche, die nicht stattgefunden haben, neben die bestandenen, denn eine grüne Zahl, die ihre eigenen blinden Flecken schluckt, ist ein hergestelltes Bestehen.
  6. Ein Grundrauschen ist verderblich, und der einzige Grund, warum das überhaupt jemand merkt, ist: Es ist eingecheckt, datiert und klein genug zum Lesen.

tests/golden/thresholds.json · 46f81ba, 2026-08-19

Was misst ein Diff-Schwellenwert eigentlich?

Ein Schwellenwert beantwortet eine einzige Frage — wie viel Unterschied bedeutsam ist — und diese Frage lässt sich nicht beantworten, solange du nicht weißt, wie viel Unterschied auftaucht, wenn sich gar nichts geändert hat. Der Großteil der veröffentlichten Ratschläge überspringt diesen Schritt komplett. Fang unter einem Prozent an, heißt es dort, und justiere nach, wenn die Suite unruhig wird.

Gegen Fehlschläge zu justieren optimiert auf eine grüne Suite, und das ist das Einzige, worauf ein Schwellenwert niemals optimiert werden darf. Dieses Repository hat da angefangen, wo alle anfangen: mit nach Augenmaß gewählten Zahlen. Seine Config hat das in der Datei selbst zugegeben — die Canvas-Zahlen waren als PROVISIONAL markiert, Platzhalter, bis ein Lauf von Referenz gegen Referenz auf derselben Maschine das echte Grundrauschen festlegt.

Dieser Lauf fand noch am selben Abend statt, in Commit b5175ae, und er hat jede Zahl in der Datei bewegt. Und zwar in die Richtung, mit der fast niemand rechnet — die Messung hat die Suite strenger gemacht, nicht lockerer, und sie hat ein Gate komplett gestrichen.

Jeder Schwellenwert in der Datei, vor der Messung und danach. Gelockert hat das Messen nichts.
Schwellenwert Geschätzt, 46f81ba Gemessen, b5175ae
pixel.default.maxDiffRatio 0.001 0.0001
pixel.byViewport.mobile-390 0.002 0.0005, weiterhin provisorisch
pixel.byViewport.tablet-768 keine Zeile 0.0005, weiterhin provisorisch
ssim.page.meanMin 0.995 0.999
ssim.page.worstMin 0.9 0.95
ssim.canvas.meanMin 0.98 null, Gate disqualifiziert
ssim.canvas.worstMin 0.85 null, Gate disqualifiziert
observation listenerTotal keine Toleranzzeile maxDelta 24

tools/golden/noise-floor.mjs:75-93

Wie misst man ein Grundrauschen?

Das Tool hat genau einen Zweck, und der steht in seiner ersten Zeile: messen, wie sehr das Instrument sich selbst widerspricht. Es serviert einen einzigen Build, nimmt die Referenz zweimal über dasselbe Capture-Skript auf, das auch die echte Suite benutzt, und vergleicht Lauf A mit Lauf B. Was dabei herauskommt, ist Rauschen, denn zwischen den beiden hat sich nichts geändert.

Der Vergleich ist bewusst dünn — zwei Zahlen pro Bildpaar, kein Diff-Bild, keine Deutung. pixelmatch liefert die Anzahl der Pixel, die einen Farbgrenzwert überschritten haben, und ein eigenes SSIM mit einem Fenster von acht Pixeln und einer Schrittweite von vier liefert den perzeptuellen Blick. Sowohl der Mittelwert als auch das schlechteste Fenster werden behalten, denn eine Shader-Regression ist meist lokal, und ein Mittelwert von 0.99 kann eine kaputte Ecke verstecken.

Drei Populationen kommen heraus, und sie werden nie vermischt: strukturelle Screenshots, rohe Canvas-Aufnahmen und Beobachtungsdrift — jedes Nicht-Pixel-Feld des Digests pro Checkpoint, das sich zwischen zwei Läufen derselben Seite unterschied. Perzentile werden als min, p50, p95 und max über jede sortierte Menge berichtet.

Der komplette Vergleich, aus tools/golden/noise-floor.mjs. Zwei Metriken pro Paar und sonst nichts.
function comparePair(a, b) {
  if (a.width !== b.width || a.height !== b.height) return { ok: false, reason: 'size-mismatch' }
  const total = a.width * a.height
  // Hier ist kein Diff-Bild erwünscht — nur die Anzahl. pixelmatch akzeptiert
  // genau für diesen Fall einen undefinierten Ausgabepuffer.
  const changed = pixelmatch(a.data, b.data, undefined, a.width, a.height, {
    threshold: 0.1,
    includeAA: false,
  })
  const score = ssim(a, b, { windowSize: 8, stride: 4 })
  return {
    ok: true,
    diffRatio: changed / total,
    changedPixels: changed,
    totalPixels: total,
    ssimMean: score.mean,
    ssimWorst: score.worst,
  }
}

Es kostet exakt das Doppelte einer Aufnahme in einem Viewport, und ein Viewport sind hier sechs bis neun Minuten Echtzeit, die Messung ist also eher eine Viertelstunde als umsonst. Ihre Ausgabe wird ins Repository eingecheckt, mit der Begründung, dass sie Beweismaterial ist — und klein.

noise-floor.json · 2026-08-19 · n=19 and n=89

Worüber waren sich zwei identische Läufe uneinig?

Dieselbe Maschine, derselbe Browser, dieselbe URL, zwei Läufe der Golden-Master-Startseite bei 1440 × 900, gestempelt auf 2026-08-19T19:33:49.368Z unter Node v22.23.1. Neunzehn strukturelle Screenshots und neunundachtzig rohe Canvas-Aufnahmen kamen von jeder Seite zurück.

Die strukturelle Population war sich über rein gar nichts uneinig: Pixel-Diff-Verhältnis min 0, p50 0, p95 0, max 0, wobei achtzehn der neunzehn Paare byte-identisch waren und das neunzehnte eine Marquee ist, die mit Absicht live aufgenommen wird, weil kontinuierliche Bewegung genau das Verhalten ist, das hier geprüft wird. Die Canvas-Population war sich über fast alles uneinig: p95 0.025829475308641974, max 0.8112244897959183, ein SSIM-Mittelwert, der auf 0.0181484156493548 fällt, und ein schlechtestes Fenster bei -0.886465409531798.

Dieser negative Wert ist ein echter Messwert und kein Fehler. SSIM läuft von -1 bis 1, so wie Wang, Bovik, Sheikh und Simoncelli es 2004 definiert haben, und der Unit-Test dieses Repositorys nagelt das fest, indem er zusichert, dass ein invertiertes Bild unter null liegt. Eine Metrik, die legitim negativ werden kann, sagt dir, dass die beiden Fenster strukturell antikorreliert sind — und genau so sieht ein Canvas aus, das in zwei zusammenhanglosen Momenten erwischt wurde.

  • 0 strukturelles Pixel-Diff-Verhältnis min, p50, p95 und max, über 19 Screenshot-Paare
  • 81% Canvas-Pixel mit Unterschied das Maximum über 89 rohe Canvas-Paare
  • 0.018148 schlechtester Canvas-SSIM-Mittelwert das geschätzte Gate hatte 0.98 verlangt
  • 447 vs. 431 listenerTotal, ein Checkpoint das einzige Nicht-Pixel-Feld, das sich bewegt hat
Das gemessene Grundrauschen, ausgelesen aus tests/golden/noise-floor.json. Zwei Läufe, dazwischen nichts geändert.

Der letzte Wert ist der Grund, das hier über Screenshots hinaus laufen zu lassen. listenerTotal las im einen Lauf 447 und im anderen 431 am eingeschwungenen Hero, ein Drift von 16, während rootFontSizePx, documentHeight und globalsLeaked über beide Läufe und über alle 21 Checkpoints einer späteren Matrix exakt gleich blieben. Die Zusicherung wurde von exakt auf tolerant heruntergestuft, mit einem maxDelta von 24 — über den beobachteten 16 und unter allem, was sich als Listener-Leck lesen ließe — und die anderen blieben exakt, weil die Messung sagte, dass sie es können.

Dieselbe Disziplin — miss die Bedingung, bevor du der Zahl traust — hat die veröffentlichte Barrierefreiheitszahl dieser Seite hervorgebracht, wo eine unkontrollierte Bedingung die Anzahl um etwa das 140-Fache aufblähte.

thresholds.json ssim.canvas · enforced false

Wenn kein Schwellenwert über dem Grundrauschen liegt, blockiere das Gate

Das geschätzte Canvas-Gate verlangte einen SSIM-Mittelwert von mindestens 0.98. Das gemessene Grundrauschen liegt bei 0.018148. Zwischen diesen beiden Zahlen gibt es keinen Schwellenwert, und kein Nachjustieren hätte einen gefunden: Eine Suite, die mit 0.98 ausgeliefert wird, wäre bei jedem Lauf durchgefallen, wäre durch Lockern repariert worden und hätte am Ende nichts über die riskanteste Fläche der Seite bewiesen.

Das Gate wurde also nicht gelockert, es wurde disqualifiziert. enforced false, meanMin null, worstMin null und ein Status-String, der die Bedingung benennt, statt mit den Schultern zu zucken: BLOCKED, needs deterministic time control on the scene side. Der Comparator hält sich im Code daran und nicht nur in der Prosa — er berechnet das SSIM bei jedem Lauf, notiert den Mittelwert, das schlechteste Fenster und dessen Koordinaten und gibt trotzdem ok zurück.

Der echte Lauf über beide Targets bestätigt das Urteil von der anderen Seite. Über die 306 Canvas-SSIM-Zeilen, die die beiden Builds vergleichen, hat der Mittelwert einen Median von 0.7284514638837428 und das schlechteste Fenster einen Median von -0.20839572391449632. Ein Gate, dessen medianes schlechtestes Fenster zwischen zwei für korrekt gehaltenen Builds negativ ist, kann eine Regression nicht von Rauschen trennen.

PASS
Der Vergleich hat stattgefunden und der Checkpoint war sich einig. Im eingecheckten Lauf kommen 120 von 132 Checkpoints mit einem Verhältnis von exakt 0.000000 zurück.
FAIL
Der Vergleich hat stattgefunden und der Unterschied lag über dem, was das gemessene Grundrauschen erlaubt. Der eine Fehlschlag in diesem Lauf ist eine veraltete Ledger-Zeile, kein Pixel.
BLOCKED
Der Vergleich hat nicht stattgefunden — das Instrument konnte das Motiv nicht stillhalten, oder kein Schwellenwert liegt über seinem Rauschen. Wird getrennt gezählt, nie in die Bestanden-Zahl eingerechnet.
informational
Die Zahl wird bei jedem Lauf berechnet und notiert und bringt den Build nie zum Scheitern. Sie ist der Beleg, der es später rechtfertigen wird, das Gate einzuschalten.
Die vier Ergebnisse, die dieser Comparator unterscheidet. Nur die ersten beiden sind Aussagen über die Seite.

BLOCKED ist nur dann vertretbar, wenn es eine benannte Bedingung zum Entsperren mitbringt, und hier ist diese Bedingung eingetroffen. Die eigene WebGL-Szene des Rebuilds bietet einen Test-Hook, der exakt ein Frame zu einem expliziten Zeitpunkt rendert, und damit wurde derselbe Inhalt Byte für Byte gate-fähig: Ein festgepinntes Frame wird jetzt über zwei unabhängige Seitenaufrufe hinweg als identisch zugesichert. Die Parität gegen das Legacy bleibt bauartbedingt eine menschliche Sichtprüfung anhand aufgenommener Standbilder, weil sich das Legacy nicht auf ein Frame festpinnen lässt.

Determinismus einzufordern hat außerdem zwei Defekte gefunden, die ein Blick auf das Bild nicht gefunden hätte. Ein Composite-Pass hat ein eingeschwungenes Frame über einen nicht geleerten Puffer geblendet, entdeckt daran, dass Frames, die bei 1,5 Sekunden und bei 9 Sekunden festgepinnt waren, byte-identisch zurückkamen — eine festgepinnte Zeit, die nichts ändert, ist ein Uniform, das keinen Shader erreicht. Diese Null-Differenz war der Bug, und ein Comparator, der nur auf zu viel Veränderung getrimmt ist, hätte Grün gemeldet.

Ein Canvas auf ein Frame festzupinnen ist genau die Fähigkeit, die ein Canvas braucht, um eine Reduced-Motion-Anfrage ehrlich zu beantworten — was WebGL und Rive prefers-reduced-motion schulden.

sha256 d8ca0518 · 5,851 B · one colour

Ein Grundrauschen von null kann heißen, dass der Objektivdeckel drauf ist

Die strukturelle Zeile dieser Messung liest sich viermal als 0, und die Thresholds-Datei zitiert sie als achtzehn von neunzehn Checkpoints byte-identisch über zwei Läufe. Beide Aussagen stimmen exakt. Wer die eingecheckten Screenshots dekodiert, sieht, worüber sie stimmen.

Die Referenz-Startseite bei initial-load ist 1440 × 900, 5.851 Byte und eine Farbe: 1.296.000 Pixel in #ff00ff. Genauso hero-settled, und hero-pointer-corner, und otot-section, und footer, und zwölf weitere. Sechzehn Checkpoints, an sechzehn verschiedenen Scroll-Positionen von y 0 bis y 17809, teilen sich einen einzigen sha256.

Das ist die dokumentierte Mask-Regel, die exakt wie spezifiziert arbeitet. Die Referenzseite trägt zwei Canvas-Flächen über den vollen Viewport, und vor jeder Aufnahme wird ein fixiertes Magenta-Overlay auf das Viewport-Rechteck jedes Canvas gepinnt, ganz oben in der Stapelreihenfolge — nie in das Canvas selbst gezeichnet, denn es über einen 2D-Kontext zu füllen zerstört seinen Inhalt und würde eine WebGL-Fläche komplett ruinieren. Zwei Canvas über den vollen Viewport ergeben an jeder Scroll-Position ein Overlay über den vollen Viewport.

Was die neunzehn strukturellen Screenshots hinter dieser Null tatsächlich enthalten, ermittelt durch Dekodieren jedes PNG und Zählen der verschiedenen RGB-Tripel.
Checkpoint Canvas-Regel Verschiedene Farben
initial-load mask 1
hero-settled mask 1
vierzehn weitere, gleicher sha256 mask 1
marquee capture, live 797
menu-open hide 14.060
menu-closed-again hide 14.080

Der Befund ist nur deshalb auffindbar, weil das Instrument aufschreibt, wo es absichtlich nicht hingeschaut hat. Jeder Screenshot-Datensatz trägt die Rechtecke, die er ausgeschlossen hat, und das Architekturdokument nennt dieses Feld als eines von zwei Dingen, die man lesen muss, bevor man irgendeinem Pixel-Ergebnis traut. Nichts im Repository ist falsch; die Stichprobe hinter einem Satz ist nur kleiner, als der Satz nahelegt.

Die zwei Checkpoints, die tatsächlich Seiten-Pixel tragen, tragen sie durch einen Zufall der Regel. menu-open und menu-closed-again verstecken ihre Canvas-Flächen, statt sie zu maskieren, weil das Canvas des Menüs in einem Container sitzt, der noch transformiert wird, während das Mask-Rechteck berechnet wird — ein fixiertes Overlay würde also danebenrutschen. Beide waren über den A- und den B-Lauf byte-identisch, und beide sind es fünf Tage später auch in einem unabhängigen Lauf über die volle Matrix noch — das ist echter Beleg, und deshalb ist die verschärfte 0.0001 richtig. Sie ruht auf zwei Bildern, und die Datei sagt achtzehn von neunzehn.

Und es ist kein Einzelfall. Dekodiert man jeden strukturellen Screenshot, der in den beiden Voll-Matrix-Manifesten genannt wird, sind 108 von 126 beim Golden Master und 114 von 132 beim Rebuild zu 100 Prozent einfarbig, über alle sechs Viewports, auf beiden Targets. Die billigste Prüfung, die es hier gibt, ist ein Einzeiler: Dekodiere eine Baseline und zähle ihre verschiedenen RGB-Werte, bevor du einer Null traust.

menu-open vs menu-closed-again · 3 of 6

Der zweite Weg, auf dem ein grüner Checkpoint über nichts sein kann

Ein Grundrauschen sagt dir, was dein Instrument auflösen kann. Es kann dir nicht sagen, ob der Checkpoint überhaupt auf irgendetwas gerichtet war, und diese beiden Fehlschläge sehen in der Zusammenfassungszeile identisch aus.

Bei drei der sechs Viewports — breakpoint-991, tablet-768 und mobile-390 — haben der Screenshot namens menu-open und der Screenshot namens menu-closed-again denselben sha256, auf beiden Targets. Der Klick, der das Menü öffnet, ist als erfolgreich vermerkt, failedInteractions steht überall auf 0, und der Pixel-Vergleich besteht in beide Richtungen mit Verhältnis 0, weil beide Targets gleichermaßen leer sind. Die Parität zwischen den beiden Builds bleibt wirklich gewahrt. Der Checkpoint, dessen erklärter Zweck ist zu beweisen, dass der Schließweg keine Reste hinterlässt, beweist auf der Hälfte der Matrix nichts.

Der Beobachtungsdatensatz enthält die Antwort, und sie ist auf zwei benachbarte Felder verteilt, von denen eines sehr viel einladender zu lesen ist als das andere.

interactions
Ein Klick auf den Hamburger, ok true, dann ein Warten von 1200 ms, ok true. Der Digest meldet failedInteractions 0 bei jedem Viewport auf beiden Targets.
navGroups.opacity
1 — was sich liest, als wäre das Menü offen, und es ist das Feld, das jeder beim Debuggen zuerst prüfen würde.
navGroups.rect
x 0, y 0, width 0, height 0. Das Element hat keine Box. Bei desktop-1440 misst derselbe Knoten 128,794 mal 60 bei x 16,656, mit einer Scale-Transform von 1,2.
pixel
Verhältnis 0 gegen das andere Target, bequem unter dem gemessenen Grundrauschen. Der Vergleich ist statistisch solide, korrekt konfiguriert — und über nichts.
Der mobile-390-Datensatz für das geöffnete Menü, Feld für Feld. Das Feld, das ein Leser zuerst prüft, ist das, welches in die Irre führt.

Die allgemeine Regel hat zwei Hälften, und kein Schwellenwert kann sie zusammenführen. Miss, wie sehr das Instrument sich selbst widerspricht, und sichere getrennt davon zu, dass jeder Checkpoint den Zustand erreicht hat, den sein Name behauptet — ein sichtbares Element, eine Box ungleich null, ein Digest-Feld, das sich zwischen dem Vorher und dem Nachher wirklich bewegt hat. Die erste Hälfte ist Rechnen über Bildern. Die zweite ist eine völlig andere Zusicherung, und sie ist die, die eine Suite davon abhält, ihr eigenes leeres Blatt zu benoten.

tests/.golden-diff/report.json · 2026-08-24

Zähle die Vergleiche, die nicht stattgefunden haben

Der eingecheckte Lauf über beide Targets liest 132 verglichen, 132 bestanden. Er liest außerdem 162 Canvas-Vergleiche, die nie stattgefunden haben, und der Comparator druckt diese Zahl mit Absicht direkt neben die Bestanden-Zahl: Ein Lauf, der 132 von 132 sagt und dabei still hundert ungemachte Vergleiche zurückhält, hat die Form eines hergestellten Bestehens.

Die größte Kategorie ist ein Crop-Artefakt — also die Seite, die sich mit sich selbst einig ist, während der Screenshot-Cropper es nicht ist. Ein Element von 13,328125 CSS-Pixeln Breite kam auf der einen Seite zu 15 mal 14 zugeschnitten zurück und auf der anderen zu 14 mal 15, weil der Element-Screenshot das Element zuerst in den sichtbaren Bereich scrollt und sich der Subpixel-Ursprung darunter verschiebt. Die Lösung ist keine Toleranz: Die CSS-Box wird gegatet und erzwungen, 468 von 468 Box-Prüfungen bestehen, während die PNG-Größe als informational notiert wird — die Box entscheidet die Seite, den Zuschnitt entscheidet der Cropper, und nur das Erste ist eine Aussage über den Build.

Die anderen beiden Kategorien sind billiger zu erklären und weit teurer im Lauf. 192 von 792 Versuchen, ein Canvas aufzunehmen, fanden überhaupt keine Box zum Fotografieren. 66 Versuche liefen nach jeweils 30 Sekunden in einen Timeout, jeder einzelne davon an einem einzigen Canvas — dem Seitenübergang der Referenz, der bei keinem seiner 66 Versuche stillhielt. Das sind mindestens 1.980 Sekunden eines Matrix-Laufs von 2.620,6 Sekunden: drei Viertel der Echtzeit damit verbracht, auf das Fotografieren von etwas zu warten, das sich nicht fotografieren ließ.

aufgenommen 534
keine Box zum Fotografieren 192
Timeout nach 30 s 66
792 Versuche, ein Canvas aufzunehmen, über den Referenz-Voll-Matrix-Lauf vom 2026-08-24.Die Balken sind maßstäblich gegen die 534 gelungenen Aufnahmen.

Die eigene Matrix des Rebuilds, mit 132 Checkpoints etwas größer, war in 4,82 Minuten fertig, gegen 43,68 bei der Referenz. Fast der gesamte Unterschied geht auf ein einziges Canvas zurück, das sich nicht auf ein Frame festpinnen lässt — und das ist zugleich ein Performance-Fakt und ein Instrumenten-Fakt: Das, was deine Suite nicht fotografieren kann, ist auch das, womit sie ihren Nachmittag verbringt.

Echtzeit ist ein eigenes Messproblem, und die Scroll-Schicht dieser Seite hat dieselbe Behandlung bekommen — Lenis gegen natives Scrolling, gemessen statt behauptet.

pixelmatch 7.2.0 · checked 2026-08-26

Was setzen die Thresholds von pixelmatch und Playwright eigentlich?

Die halbe Verwirrung um Schwellenwerte kommt daher, dass das Wort zwei verschiedene Zahlen benennt. In pixelmatch — der Bibliothek unter diesem Vergleich und dem Modell, dem Playwrights eigene Screenshot-Zusicherung folgt — ist threshold ein Farbabstands-Grenzwert pro Pixel und keine Toleranz pro Bild. In Version 7.2.0, der hier gepinnten, wird threshold 0.1 zu einem maximalen quadrierten YIQ-Abstand von 35215 × 0.1 × 0.1 = 352.15, gegen einen maximal möglichen Wert von 35215. Die Funktion gibt dann die Anzahl der Pixel zurück, die ihn überschritten haben.

Die Toleranz pro Bild ist eine eigene Zahl, und sie ist die, die dir ein Grundrauschen nennt. Playwright macht die Trennung sichtbar und lässt die eine Hälfte leer: threshold liegt standardmäßig bei 0.2, während maxDiffPixels und maxDiffPixelRatio überhaupt keinen Standard haben — ab Werk lässt also ein einziges hinreichend abweichendes Pixel die Zusicherung scheitern. Vitest liefert ebenfalls keine Standardtoleranz und sagt das auch offen, mit der Empfehlung, ein Verhältnis aus der Größe des Screenshots zu berechnen statt einer festen Anzahl.

Noch etwas, das das Wort verbirgt: Die Metrik darunter ist über Versionen hinweg nicht stabil. pixelmatch hat auf seinem main-Branch den YIQ-Abstand durch die OKLab-HyAB-Metrik ersetzt, threshold 0.1 ist über die Zeit also keine feste Größe. Und includeAA liest sich rückwärts zu seinem Namen — false, der Standard und die Einstellung dieses Repositorys, bedeutet, dass die Antialiasing-Erkennung an ist und AA-Pixel nicht als Unterschiede zählen.

Was die Standardwerte jedes Tools setzen, und wo die zweite Zahl schlicht fehlt.
Tool Grenzwert pro Pixel Toleranz pro Bild
pixelmatch 7.2.0, dieses Repository threshold 0.1, YIQ quadriert maxDiffRatio 0.0001, gemessen
pixelmatch, main-Branch threshold, OKLab HyAB keine, es gibt eine Anzahl zurück
Playwright toHaveScreenshot threshold 0.2, YIQ standardmäßig nicht gesetzt
Vitest Browser-Mode nicht zugänglich kein Standard, ein Verhältnis empfohlen

Keiner dieser Standardwerte weiß irgendetwas über deine Seite, und genau darum misst man die eigene. Playwright und Vitest sagen dir beide, Baselines in der Umgebung neu zu erzeugen, in der du sie vergleichen wirst, und jede oben zitierte Zahl stammt von einer Maschine, einem Chromium-Build, headless, ohne Determinismus-Flags bei der Aufnahme — kein font-render-hinting, kein erzwungener Device-Scale. Die Toleranz von 0.0001 wird in der Datei als Spielraum für Font-Rasterisierung über Maschinen hinweg beschrieben, also als Spielraum für einen Fall, den dieses Repository noch nicht gemessen hat.

noise-floor.json · one commit, never re-run

Wie du einen Schwellenwert ohne Raten setzt

Das Verfahren ist kurz, und das meiste daran ist Buchhaltung. Tragfähig wird es dadurch, dass die Ausgabe neben den Zahlen eingecheckt wird, die sie begründet — ihr Alter steht damit in einem git log statt in irgendjemandes Gedächtnis.

  1. Einen Build servieren, zweimal aufnehmen Zwischen den beiden Läufen darf sich nichts unterscheiden außer dem Moment, in dem sie passiert sind. Nimm dasselbe Capture-Tool, das auch die echte Suite benutzt, statt eines vereinfachten Ersatzes, sonst hast du ein anderes Instrument gemessen.
  2. A gegen B diffen, über alles, was du zusicherst Anzahl pro Pixel, eine perzeptuelle Metrik mit behaltenem schlechtestem Fenster zusätzlich zum Mittelwert, und jedes Nicht-Pixel-Feld, das dein Digest festhält. listenerTotal hat sich hier um 16 bewegt; documentHeight hat sich gar nicht bewegt.
  3. Jeden Schwellenwert über sein eigenes p95 setzen Mit einem Spielraum, den du bewusst wählst, und mit der Messung als Kommentar neben der Zahl. Ein Schwellenwert, den niemand zurückverfolgen kann, ist ein Schwellenwert, den jeder lockern kann.
  4. Wo nichts über dem Rauschen liegt, das Gate blockieren Notiere die Zahlen, lass den Build nie daran scheitern und benenne die konkrete Änderung, die es entsperren würde. Ein blockiertes Gate mit einem Weg zurück ist ehrlich; ein gelockertes ist Dekoration.
  5. Die Messung einchecken, datiert und klein Als Text, im Repository, neben den Schwellenwerten, die sie hervorgebracht hat. Nur so merkt der nächste Leser, dass sie veraltet ist, statt ihr zu vertrauen.
  6. Prüfen, ob das Instrument das Motiv gesehen hat Dekodiere eine Baseline und zähle ihre verschiedenen RGB-Werte. Eine Farbe heißt, du hast die Maske fotografiert. Lies danach, was dein Tooling über die ausgeschlossenen Regionen festhält.
  7. Zusichern, dass jeder Checkpoint seinen Namen eingelöst hat Ein sichtbares Element, eine Box ungleich null, ein Digest-Feld, das sich zwischen dem Vorher und dem Nachher geändert hat. Kein Schwellenwert impliziert irgendetwas davon, und eine grüne Suite ohne das benotet leeres Papier.
Das ganze Verfahren, einschließlich der zwei Schritte, die gar nichts mit Pixeln zu tun haben.

Zwei Vorbehalte, die dieses Repository an sich selbst vorführt, beide in seiner eigenen Config sichtbar. Zwei seiner sechs Viewports, beide mit deviceScaleFactor 2, haben nie ein gemessenes Grundrauschen bekommen, und ihre Schwellenwerte tragen das Eingeständnis genau dort, wo der nächste Leser darauf stößt: noch nicht gemessen, provisorisch. Das Architekturdokument nennt die Konsequenz ohne Weichzeichner — ein Bestehen oder ein Scheitern bei diesen zwei Viewports beweist nichts über das Instrument, und kein Kandidat sollte darauf abgenommen werden.

Und das Grundrauschen selbst hat genau einen Commit. Es wurde am 2026-08-19 erhoben und nie wiederholt, während sich das gemessene Szenario seither zweimal geändert hat und von 19 auf 21 Checkpoints gewachsen ist. Ein Grundrauschen ist verderblich, es zerfällt, sobald sich die Seite, der Browser oder die Maschine bewegt, und der einzige Grund, warum überhaupt jemand sagen kann, wie alt dieses hier ist, ist eine kleine datierte Datei in der Versionsverwaltung, direkt neben den Zahlen, die sie hervorgebracht hat.

Dasselbe Argument hat auf dieser Seite eine typografische Zahl entschieden, wo ein Verhältnis, das alle nachplappern, ersetzt wurde durch eine aus den Umrissen berechnete Zeilenhöhen-Untergrenze.

Instrumentenarbeit wie diese gehört dazu, wie die Seiten hier gebaut werden, beschrieben in den vier Disziplinen.

Fragen

Wie viele Läufe brauche ich für ein brauchbares Grundrauschen?

Zwei Läufe haben hier gereicht, weil das Signal in beide Richtungen eindeutig war: Der strukturelle Durchgang kam byte-identisch zurück und der Canvas-Durchgang mit einem Maximum von 81 Prozent. Zwei Läufe reichen, um ein Instrument zu disqualifizieren, und sie reichen, um das Verschärfen eines Schwellenwerts zu rechtfertigen, der ohnehin schon null liest. Sie reichen nicht, um eine Verteilung zu charakterisieren — die Perzentile in der eingecheckten Datei liegen über 19 und 89 Stichproben aus einem einzigen Läufe-Paar, und nichts im Repository behauptet mehr. Ein Grundrauschen, das irgendwo in der Mitte landet, ist der Fall, in dem du mehr Läufe brauchst, nicht weniger.

Mein Grundrauschen kam mit null zurück. Ist das gut?

Prüfe, was im Bild ist, bevor du feierst. Das strukturelle Grundrauschen ist hier exakt null, und sechzehn der neunzehn Screenshots dahinter sind 1440 × 900 in einer einzigen Farbe, weil das Capture-Tool Canvas-Flächen maskiert und die Seite zwei davon über den vollen Viewport trägt. Der aus diesem Lauf abgeleitete Schwellenwert stimmt trotzdem, aber er ruht auf zwei echten Screenshots statt auf neunzehn. Dekodiere eine Baseline, zähle ihre verschiedenen RGB-Werte und lies, was dein Tooling über die ausgeschlossenen Regionen festhält.

Was tue ich, wenn kein Schwellenwert über dem Grundrauschen liegt?

Sag es in der Config und hör auf zu gaten. Dieses Repository schreibt enforced false, meanMin null, worstMin null und einen Status BLOCKED mit angehängtem Grund, und sein Comparator berechnet das SSIM trotzdem, notiert den Mittelwert, das schlechteste Fenster und dessen Koordinaten und gibt trotzdem ok zurück. Die Begründung steht in der Datei: Ein Gate, das eine Regression nicht von Rauschen unterscheiden kann, ist schlimmer als gar kein Gate, weil es Leuten beibringt, Rot zu ignorieren. Entscheidend ist, dass BLOCKED die Änderung benennt, die es entsperren würde — und als diese Änderung auf der Rebuild-Seite ankam, wurde derselbe Inhalt Byte für Byte gate-fähig.

Lassen sich Canvas- oder WebGL-Inhalte je pixelgenau diffen?

Ja, genau dann, wenn der Inhalt keine ungepinnte Abhängigkeit von der Zeit hat. Ein Punktfeld, dessen Seed aus einem Hash ganzzahliger Gitterkoordinaten stammt, ohne Zufallsquelle, ohne Zeit-Seed und ohne Datum, wurde unter reduzierter Bewegung pixelgenau verglichen und kam auf beiden Seiten mit 365 gemalten Pixeln zurück, an denselben 365 Koordinaten, mittlerer Alpha-Wert 53,742 auf beiden und einer Alpha-Differenz pro Pixel von 0. Dasselbe Feld, animiert, bewegte sich über fünf Aufnahmen im Abstand von 400 ms auf einem Target zwischen 630 und 1.841 gemalten Pixeln — allein aus dem Moment des Auslösens heraus. Determinismus ist eine Eigenschaft der Zeitabhängigkeit des Inhalts, nicht der Fläche.

Heißt ein gemessenes Grundrauschen, dass ich die Umgebung nicht mehr stabilisieren muss?

Im Gegenteil: Das Grundrauschen gilt nur für die Umgebung, in der es gemessen wurde, und es ist genau das, was dir sagt, ob das Stabilisieren funktioniert hat. Dieses hier wurde auf einer einzigen Maschine erhoben, in einem Chromium-Build, ganz ohne Determinismus-Flags, es sagt also nichts über eine zweite Maschine. Zwei der sechs Viewports in dieser Matrix haben nie ein gemessenes Grundrauschen bekommen, und ihre Schwellenwerte sagen das in der Config. Playwright und Vitest geben in ihren eigenen Docs denselben Rat: Erzeuge Baselines in der Umgebung neu, in der du sie vergleichen wirst.

Quellen

In diesem Repository gemessen

  • tests/golden/noise-floor.json Die Messung selbst: 19 strukturelle Paare, 89 Canvas-Paare, Perzentile pro Population und das eine Nicht-Pixel-Feld, das gedriftet ist. Ein Commit, 2026-08-19.
  • tests/golden/thresholds.json Jeder Schwellenwert mit der Begründung, aus der er abgeleitet wurde, das disqualifizierte Canvas-Gate und die zwei Viewports, die weiterhin als provisorisch markiert sind.
  • tools/golden/noise-floor.mjs Das Tool: ein Build zweimal aufgenommen, pixelmatch für die Anzahl und ein eigenes SSIM mit Fenster 8, Schrittweite 4, behalten als Mittelwert und schlechtestes Fenster.
  • tests/.golden-diff/report.json Der eingecheckte Lauf über beide Targets: 132 verglichen, 132 bestanden, 162 Canvas-Vergleiche nicht bewertet, 468 von 468 Box-Prüfungen erzwungen und bestanden.
  • docs/adr/ADR-013-golden-master-testing.md Die geprüften Alternativen und warum jede verloren hat, darunter ein Screenshot pro Checkpoint mit einer Toleranz, die so lange justiert wird, bis er besteht.

Geprüft gegen

Alaa Abbod

Geschrieben von

Alaa Abbod

Creative Developer — Herne, Deutschland

Designer und Entwickler, der barrierefreie Websites, Mobile Apps, Onlineshops und visuelle Identitäten als eine Arbeit baut, von Hand. Diese Seite erscheint in Englisch, Deutsch und Arabisch aus einer Quelle, und daher kommen die meisten dieser Fragen.

Bitte dreh dein Gerät,
Diese Seite ist hochkant gebaut.