| Component name | canvas_offscreen |
|---|---|
| In the visitorId | no (reported, kept out of both ids) |
| structuredId group | none |
| Hashing | plain value; a digest only when longer than 128 characters or not printable ASCII |
| Excluded under | no browser class |
| Inner timeout | 1000 ms |
| Added in | signalsVersion 14 |
| Collector revision | 1 (reported as revision with every result) |
Web pages can draw on two kinds of canvas: the normal one on the page and an “offscreen” one that is not shown. A browser draws both with the same engine, so the same drawing gives the same pixels. Tools that fake canvas fingerprints often change only the normal one. WhiskerJS draws the same small picture on both and reports whether they match.
What it reads
The scene of the canvas component (text, emoji, blending and winding, 260 by 80 pixels), drawn again on a DOM canvas and on an OffscreenCanvas created on the main thread, both with willReadFrequently so that both take the same CPU raster path, and read with getImageData. Nothing is shown on the page, and no worker is used.
How the technique works
| Step | What happens | Value |
|---|---|---|
| 1 | draw on both, compare the pixels byte for byte | equal: same |
| 2 | they differ: draw a second pair | |
| 3 | a canvas differs from its own first render | per-read noise: noisy / render_mismatch |
| 4 | each canvas repeats its own render, the two still differ | differs |
differs is what the canvas_realm_mismatch flag reads.
Why it helps
A script or extension that patches only HTMLCanvasElement or CanvasRenderingContext2D (and not OffscreenCanvas and its context) makes the two differ; canvas-spoofing extensions often patch only those. On an honest browser the answer is always same, so the component carries no entropy: its value is the comparison.
Stability
same in every collection on an honest browser. Reported outside both ids. Added in library PR #35 (2026-10-27) at signalsVersion 14: it is outside both ids, so the ids did not change; the version moved because of the new flag.
Browser support and what blocks it
- In the self-hosted CI browser matrix of PR #35 (23 browsers and channels on Linux, load, reload and a new context) every browser reported
samein all three collections, Brave and Brave beta included (Brave farbles both kinds of canvas the same way within a session), except Playwright’s Firefox with resistFingerprinting:noisy/render_mismatch, because its canvas noise is drawn on every read. unavailable/no_apiwithoutOffscreenCanvas: Safari before 16.4, Firefox before 105.- No browser class excludes it; the flag skips the classes that alter canvas readback themselves.
Privacy
It draws a fixed test picture twice, off screen, and keeps only the answer same or differs; the pixels never leave the library. No prompt, no request. Leave it out with collect({ exclude: ['canvas_offscreen'] }) if you do not need it; no id changes, and canvas_realm_mismatch then never fires.
How WhiskerJS handles noise and failures
| Status | ruleId | When |
|---|---|---|
ok |
same or differs |
|
unavailable |
no_api |
no OffscreenCanvas |
unavailable |
no_document |
no document |
unavailable |
no_context |
the DOM canvas gave no 2D context or no pixels |
unavailable |
no_offscreen_context |
the OffscreenCanvas gave no 2D context or no pixels |
noisy |
render_mismatch |
a canvas differed from its own first render |
blocked |
empty_image |
the second pair could not be read |
blocked |
threw, security_error |
a call threw |
excluded |
caller_excluded |
you excluded it |
timed_out |
timeout, aborted |
budget or abort |
Example value
"canvas_offscreen": { "status": "ok", "value": "same", "hashed": false, "durationMs": 9.8, "revision": 1 }
Questions and answers
Does canvas_offscreen use a worker?
No. The OffscreenCanvas is created on the main thread. WhiskerJS never builds a worker, because a page's security policy can block it and report the block over the network.
Is it excluded in Brave?
No. Brave farbles both kinds of canvas the same way within a session, so in the CI matrix it reported same. The canvas_realm_mismatch flag is never checked for Brave and the other browsers that alter canvas readback themselves.
Why is it not in the visitorId?
It is a yes or no answer about the page, not a device trait: same on every honest browser.