# How WhiskerJS compares, feature by feature

> Which signals each open-source library collects, under which licence, what each one says about its own stability and uniqueness, and which ones send data home by default.

Source: https://whiskerjs.com/compare/ (last updated 2026-10-26)

> **No head-to-head measurement yet.** This page compares features: what each library reads. It does not say that WhiskerJS is more accurate, more stable or more unique than any other library, because the measurement that could show that has not run yet. A measurement harness that runs the libraries side by side on the same devices is coming, and its results will be published here.

How this was made: each library's public repository was read at the version below on 2026-10-24 (code read, never installed or run). The stability and uniqueness column quotes what each project publishes about itself; "not published" means it gives no figure. Projects are linked to their own pages. No logos are used. If something here is out of date or wrong, it is a reading error on our side; the project's own documentation is the reference.

## Pair by pair

Longer, feature-only comparisons with the libraries the research covers in most detail, and an overview of the open-source options:

- [Open-source alternatives](https://whiskerjs.com/compare/open-source-alternatives/): Every library read, what kind it is, licence and network defaults.
- [WhiskerJS and FingerprintJS](https://whiskerjs.com/compare/fingerprintjs/): Noise skips and confidence score against double render and statuses.
- [WhiskerJS and ThumbmarkJS](https://whiskerjs.com/compare/thumbmarkjs/): Common-pixel reduction and sampled logging against no network.
- [WhiskerJS and ClientJS](https://whiskerjs.com/compare/clientjs/): A 32-bit hash with the full user agent against versioned ids.
- [WhiskerJS and BotD](https://whiskerjs.com/compare/botd/): A bot verdict against flags without a verdict.
- [WhiskerJS and fpscanner](https://whiskerjs.com/compare/fpscanner/): Sectioned fsid against structuredId; obfuscation against a digest.

## The libraries

| Key | Library | Version read | Licence | Last release | Status |
| --- | --- | --- | --- | --- | --- |
| **WJ** | [WhiskerJS](https://whiskerjs.com/docs/) | 0.1.0 (signalsVersion 5) | Apache-2.0 | not yet published | In development |
| **F5** | [FingerprintJS](https://github.com/fingerprintjs/fingerprintjs) | 5.3.0 | MIT | 2026-10-07 | Maintained |
| **F3** | [FingerprintJS](https://github.com/fingerprintjs/fingerprintjs) | 3.4.2 | MIT | 2023-07-06 | Historical |
| **F4** | [FingerprintJS](https://github.com/fingerprintjs/fingerprintjs) | 4.6.2 | BUSL-1.1 | 2025-04-10 | Historical |
| **TM** | [ThumbmarkJS](https://github.com/thumbmarkjs/thumbmarkjs) | 1.12.0 | MIT | 2026-09-25 | Maintained |
| **CJ** | [ClientJS](https://github.com/jackspirou/clientjs) | 0.2.1 | Apache-2.0 | 2021-10-25 | Unmaintained |
| **BD** | [BotD](https://github.com/fingerprintjs/BotD) a bot classifier, not a device identifier | 2.0.0 | MIT | 2025-11-04 | Maintained (stability-only roadmap) |
| **FS** | [fpscanner](https://github.com/antoinevastel/fpscanner) | 1.0.8 | MIT | 2026-08-05 | Maintained |
| **RS** | [A browser fingerprinting research site](https://github.com/abrahamjuliot/creepjs) its name is a claimed trade mark, so this page describes it instead of naming it | commit 10aa672 | MIT, plus a trade mark policy on its name | no releases; last push 2026-06-11 | Maintained as a research site |
| **FR** | [fingerprinter-js](https://github.com/Lorenzo-Coslado/fingerprinter-js) | 2.0.1 | MIT | 2026-01-30 | Maintained, small |
| **GB** | [get-browser-fingerprint](https://github.com/damianobarbati/get-browser-fingerprint) | 5.2.4 | MIT in package.json, no LICENSE file | 2026-04-02 | Maintained, small |
| **DU** | [device-uuid](https://github.com/biggora/device-uuid) | 3.0.6 | MIT | 2026-09-28 | Maintained |
| **TD** | [TrustDevice-JS](https://github.com/trustdecision/trustdevice-js) | 1.0.6 | MIT | 2025-03-07 | Historical |

Not compared here because they are not browser collectors: JA4 (a TLS fingerprint computed at the network edge; WhiskerJS does not compute it), and Bowser and ua-parser-js (they parse a User-Agent string; WhiskerJS reports raw signals and leaves parsing to the receiver).

## Signals collected

**Y** collected for the identifier · **b** used only as a bot, lie or consistency check · **o** opt-in, or collected but outside the hash · **-** not collected. Each column is as of the version in the table above. Scroll sideways on a small screen.

| Signal | WJ | F5 | F3 | F4 | TM | CJ | BD | FS | RS | FR | GB | DU | TD |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| Canvas 2D image | Y (double render, tamper probe) | Y (noise skip, double render) | Y | Y | Y (3 renders, common pixel) | Y | - | Y (+ tamper check) | Y (image, blob, paint, text, emoji) | Y | Y | o | Y |
| WebGL vendor and renderer (unmasked) | Y | Y | Y | Y | Y | - | b | Y | Y | Y | Y | o | Y |
| WebGL parameters and extensions | Y | Y | - | Y | - | - | - | - | Y | Y (few) | - | o | - |
| WebGL rendered image | Y (double render) | - | - | - | Y | - | - | - | Y | - | - | - | - |
| WebGPU adapter info | Y | - | - | - | - | - | - | Y | Y | - | Y | - | - |
| Audio (OfflineAudioContext) | Y | Y | Y | Y | Y | - | - | - | Y | o | Y | o | Y |
| Audio base latency | o (Safari and Android only) | Y | - | Y | - | - | - | - | - | - | - | - | - |
| Fonts (how probed) | Y (54, span width and canvas measureText) | Y (52, span width, iframe) | Y (52, span) | Y | Y (89, canvas measureText, iframe) | Y (231, span) | - | - | Y (document.fonts and FontFace local()) | Y | Y (span width) | o (span and canvas) | Y |
| Font preferences (default font sizes) | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - |
| Screen size and colour depth | Y | Y | Y | Y | Y | Y | b | Y | Y | Y | Y (size mobile only) | Y | Y |
| Screen frame or available area | o | Y | Y | Y | - | Y | b (outer size) | Y | Y | - | - | - | - |
| Time zone | Y | Y | Y | Y | Y | Y (offset) | - | Y | Y | Y | Y | o | Y |
| Intl locale and calendars | Y | Y | - | Y | - | - | - | Y | Y | - | Y | - | - |
| Languages | Y | Y | Y | Y | Y | Y | b | Y | Y | Y | Y | Y | Y |
| User-Agent and platform | Y (platform; the UA string only for the class and checks) | Y | Y | Y | Y | Y | b | Y | Y | Y | Y | Y | Y |
| UA Client Hints (high entropy) | Y (versions outside the id) | Y | - | - | - | - | - | Y | Y | Y | Y | - | Y |
| Hardware concurrency | Y | Y | Y | Y | Y | - | - | Y | Y | Y | Y | Y | Y |
| Device memory | Y | Y | Y | Y | Y | - | - | Y | Y | Y | Y | Y | Y |
| JS heap limit | Y | - | - | - | Y | - | - | - | - | - | - | - | - |
| CPU architecture (NaN trick or CH) | Y (NaN) | Y | Y | Y | - | - | - | Y (CH) | - | - | Y (CH) | - | - |
| Touch support | Y | Y | Y | Y | Y | - | - | - | Y | Y | Y | Y | Y |
| Plugins and mimeTypes | Y (+ consistency check) | Y | Y | Y | Y | Y | b | Y | Y (+ lies) | Y | - | - | Y |
| Math precision | Y | Y | Y | Y | Y | - | - | - | Y | Y | - | - | - |
| CSS media features | Y (5) | Y (8) | Y (7) | Y (8) | Y (11) | - | - | Y (9) | Y | - | Y (5) | - | - |
| Storage availability | Y | Y | Y | Y | - | Y | - | - | Y (quota) | Y | - | - | Y |
| Cookies enabled | Y | Y | Y | Y | Y | Y | - | - | - | - | - | - | Y |
| Media codecs | Y | - | - | - | - | - | - | Y | Y | - | Y | - | - |
| Speech voices | o | - | - | - | Y | - | - | - | Y | - | - | - | - |
| Permissions states | o (24) | - | - | - | Y (28) | - | b | - | Y | o | Y (2) | - | - |
| Media device counts | - | - | - | - | - | - | - | Y | Y | Y | - | o | - |
| WebRTC | - | - | - | - | Y (SDP, no IP) | - | - | - | Y | o (IPs via Google STUN) | - | - | - |
| Network information | - | - | - | - | - | - | b (rtt) | - | Y | o | Y | o | - |
| Battery | - | - | - | - | - | - | - | - | Y | o | - | - | - |
| Keyboard layout | o | - | - | - | - | - | - | Y | - | - | - | - | - |
| Content blockers or extensions | o (8 bait elements) | Y (filter lists) | Y | Y | - | - | - | Y (8 extensions) | Y (privacy resistance) | - | - | - | - |
| Vendor-specific globals | Y | Y | Y | Y | Y (Apple Pay) | Y (Java, Flash) | b | Y | Y | - | - | - | - |
| Apple Pay, Private Click Measurement | Y | Y | - | Y | Y (Apple Pay) | - | - | Y (ApplePaySetup) | - | - | - | - | - |
| PDF viewer | Y | Y | Y | Y | - | - | - | - | Y | - | Y | - | - |
| Automation markers (webdriver, Selenium, Playwright, CDP, PhantomJS) | b (webdriver, 32 globals, headless; no CDP) | - | - | - | - | - | b (18 kinds) | b (21 rules, CDP, Playwright) | b (headless, stealth) | b | b (webdriver) | b (bot UA) | b (webdriver) |
| Lie or tamper detection | b (native source, descriptors, clean iframe, Proxy) | b (canvas noise) | - | b (canvas noise) | - | - | b (consistency) | b (descriptors, canvas tamper, context mismatch) | b (prototype and Proxy lies, about 25 checks) | - | - | - | - |
| iframe and Worker cross-check | b (iframe; no Worker) | Y (iframe for fonts only) | - | - | Y (iframe for fonts) | - | - | b (iframe and Worker) | b (iframe and Worker) | - | - | - | - |
| Anti-replay (nonce, time) | Y | - | - | - | - | - | - | Y | - | - | - | - | - |
| Identifier format | 32-hex visitorId plus 7-part structuredId | 32-hex hash | hash | hash | hash | 32-bit int | none (bot verdict) | structured `fsid` (JA4-like) | hash | SHA-256 | 32-bit hex | MD5 | MD5 in cookie |

WhiskerJS's column describes main at signalsVersion 5: 46 components, 37 of them in the visitorId across 7 groups, and 14 integrity checks. Every one is listed on [Signals](https://whiskerjs.com/docs/signals/) and [Integrity checks](https://whiskerjs.com/docs/integrity/). WhiskerJS leaves out WebRTC, media device counts, network information, battery and Chrome DevTools Protocol detection on purpose: they need a network-capable API, a permission-coupled API or console output.

## Stability and uniqueness, in each project's own words

These are each project's own claims, quoted from its README or docs at the version read. They were not measured by us, and they are not comparable with each other: each project uses its own method and audience, or gives no figure.

| Library | Stability (their words) | Uniqueness (their words) |
| --- | --- | --- |
| **WhiskerJS 0.1.0** | Not published yet. Ids are versioned (signalsVersion and idVersion) and change when a version bumps. | Not published yet: waiting for the measurement harness. |
| **FingerprintJS 5.3.0** | Docs (version policy): "The library tries to keep visitor identifiers the same within a minor version ... Some visitor identifiers may change within a minor version due to stability fixes." Noisy sources are skipped where noise is known. | README: "the accuracy is significantly lower than in the commercial version" (no figure). The code returns a fixed confidence score by platform, from 0.3 to 0.7. |
| **FingerprintJS 3.4.2** | Same version policy as 5.x. | README: "limited accuracy (40% - 60%)", and "Accuracy: up to 60%" in its comparison table. |
| **FingerprintJS 4.6.2** | Same version policy as 5.x. | README: "the accuracy is limited (40% - 60%)". |
| **ThumbmarkJS 1.12.0** | Option `stabilize`, default `['private', 'iframe']`: "Preset exclusion list for stability across private browsing and iframes". | README: "The client library achieves ~80% uniqueness across general browser populations. Mac/Safari users can show higher collision rates or higher noise depending on the audience." |
| **ClientJS 0.2.1** | Not published. | README: "It is not a full proof technique" (no figure). |
| **BotD 2.0.0** | Not applicable (a classifier). | Not published. |
| **fpscanner 1.0.8** | Not published; its structured `fsid` allows partial matching. | Not published. README: "Fingerprints are representations, not verdicts." |
| **Research site** | Not published as figures; it excludes APIs it detects as lied about. | Not published. |
| **fingerprinter-js 2.0.1** | README: "Only stable data contributes to fingerprint hash". | README gives per-collector entropy estimates, for example canvas "~10 bits". |
| **get-browser-fingerprint 5.2.4** | README: "The script does not try to bypass intentional randomization or hardening." | Not published. |
| **device-uuid 3.0.6** | Not published. | Not published. |
| **TrustDevice-JS 1.0.6** | README: "Will remain the same after uninstalling and reinstalling or clearing browser data." (it caches the id in a cookie) | Not published. |

## Which send data home by default

| Library | Default network behaviour | Off switch |
| --- | --- | --- |
| **FingerprintJS 5.3.0, 4.6.2, 3.4.2** | On by default, about 1 in 1000 loads: a GET to the vendor's monitoring host ("installation statistics"). | `load({ monitoring: false })` |
| **BotD 2.0.0** | On by default, about 1 in 1000 loads: a GET to the vendor's monitoring host. | `load({ monitoring: false })` |
| **ThumbmarkJS 1.12.0** | On by default, about 1 in 10000 runs: a POST of all components and the page path to its logging API, which on the same run also fetches and evaluates a remote script. With an API key, further calls to its API and a beacon. | `logging: false`, no `api_key`, `collect_beacon: false` |
| **fingerprinter-js 2.0.1** | No telemetry found. Its WebRTC collector (outside the hash) contacts a Google STUN server. | skip the WebRTC collector |
| **The research site** | Fetches its own script and a samples file from its own site (it is a website, not a packaged library). | not applicable |
| **ClientJS, fpscanner, get-browser-fingerprint, device-uuid, TrustDevice-JS** | No network call found in the code read. | not needed |
| **WhiskerJS 0.1.0 (main, signalsVersion 5)** | No network call of any kind. The test suite scans the compiled code and records every call on a fake browser. | not needed |

## Measured so far: WhiskerJS's own early lab runs

> **An early, small sample, not a benchmark.** These runs used WhiskerJS 0.1.0 at **signalsVersion 1** (the first collector, 30 components), not the current signalsVersion 5. They involved no other library. They show only that the id held across reloads on these four device and browser pairs.

| Device and browser | Runs | Identifiers seen |
| --- | --- | --- |
| Mac, Chrome | 4 | 1 (the same in every run) |
| Mac, Safari | 2 | 1 (the same in every run) |
| iPhone, Chrome | 3 | 1 (the same in every run) |
| iPhone, Safari | 3 | 1 (the same in every run) |

- Each pair kept one identifier across all its runs.
- The four identifiers were all different from each other.
- `device_memory` was `unavailable` on Safari and on iOS, by design: only Chromium-based browsers offer `navigator.deviceMemory`, and Chrome on iPhone runs on WebKit.

Twelve runs on four pairs say nothing about how WhiskerJS behaves across many devices, after browser updates, or against other libraries. The measurement harness will answer those questions with numbers; until then, you can measure it on your own devices with the lab mode of the [live demo](https://whiskerjs.com/demo/#lab).

- [Try the live demo](https://whiskerjs.com/demo/): The real library, in your browser. Nothing is sent.
- [Every signal](https://whiskerjs.com/docs/signals/): What each of the 46 components reads.
- [What's new](https://whiskerjs.com/whats-new/): signalsVersion 1 to 5, idVersion 1 to 2.
