# canvas_offscreen

> The canvas scene drawn on a DOM canvas and on a main-thread OffscreenCanvas, compared byte for byte: same or differs. Reported, not in either id.

Source: https://whiskerjs.com/signals/canvas-offscreen/ (last updated 2026-10-27)

**canvas_offscreen at a glance (from the library code, signalsVersion 14)**

| Field | Value |
| --- | --- |
| 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](https://whiskerjs.com/whats-new/) |
| Collector revision | [1](https://whiskerjs.com/docs/api/#component) (reported as `revision` with every result) |

> **In plain words:**
>
> 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`](https://whiskerjs.com/signals/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`](https://whiskerjs.com/docs/integrity/#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 `same` in 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_api` without `OffscreenCanvas`: 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

```json
"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.

## Related pages

- [canvas](https://whiskerjs.com/signals/canvas/): The same scene, as a digest, in the id.
- [canvas_readback](https://whiskerjs.com/signals/canvas-readback/): Whether a known drawing reads back unchanged.
- [canvas_realm_mismatch](https://whiskerjs.com/docs/integrity/#canvas_realm_mismatch): The flag that reads it.
