ClientJS collects browser environment signals — user agent, screen geometry, installed fonts, canvas rendering, timezone — and hashes them into a 32-bit integer you can use as an anonymous visitor identifier. It also exposes every individual signal as its own getter so you can pick and choose your own combination.
Why I starred it
Most fingerprinting libraries hand you a number and hide the logic. ClientJS does the opposite: every data point it feeds into the hash is available as a named method on the same object. That's useful when you want to understand why two fingerprints diverged, or when you want a custom hash from a specific subset of signals.
The 55 KB full bundle and 28 KB base bundle are modest by today's standards, and it has zero runtime dependencies in the browser — ua-parser-js, murmurhash-js, globalthis, and inherits are all bundled at build time.
How it works
The entry point is src/client.base.js. The constructor is three lines:
var ClientJS = function() {
var parser = new UAParser;
browserData = parser.getResult();
fontDetective = new Detector();
return this;
};
UAParser does the heavy lifting for browser, OS, device, engine, and CPU detection — ClientJS wraps its output with thin accessors rather than re-implementing UA parsing itself. That's the right call: UA parsing is a maintenance nightmare and ua-parser-js has 20 years of regex debt so you don't have to.
The fingerprint itself lives in getFingerprint() and is straightforward — concatenate all signals with | separators, then run murmurhash3_32_gc with a fixed seed of 256:
getFingerprint: function() {
var key = userAgent + bar + screenPrint + bar + pluginList + bar
+ fontList + bar + localStorage + bar + sessionStorage + bar
+ timeZone + bar + language + bar + systemLanguage + bar
+ cookies + bar + canvasPrint;
var seed = 256;
return murmurhash3_32_gc(key, seed);
}
The canvas fingerprint (getCanvasPrint) draws a specific text string — 'ClientJS,org <canvas> 1.0' — with a deliberate mix of fill colors and slight offsets on a Canvas element, then captures the full data URI. GPU rendering differences across machines mean the pixel output differs subtly even for the same browser version.
Font detection is vendored from Lalit Patel's 2009 library (src/vendor/fontdetect.js). It works by rendering a test string (mmmmmmmmmmlli) at 72px in the target font against all three CSS generic families (monospace, sans-serif, serif), and flags a font as installed if the measured width diverges from the baseline. It's a classic technique — no canvas required — and it checks 200+ fonts by default.
The build outputs four separate bundles (client.min.js, client.flash.min.js, client.java.min.js, client.base.min.js) to let you exclude Flash/Java detection depending on your target environment. Java and Flash detection are split into src/modules/java-detection.js and src/modules/flash-detection.js and grafted onto the base class via prototype inheritance using the inherits package.
Using it
import { ClientJS } from 'clientjs';
const client = new ClientJS();
// Default fingerprint — 32-bit integer
const fp = client.getFingerprint();
// Custom fingerprint from specific signals only
const custom = client.getCustomFingerprint(
client.getUserAgent(),
client.getTimeZone(),
client.getLanguage()
);
// Individual signals
console.log(client.getBrowser()); // "Chrome"
console.log(client.getOS()); // "macOS"
console.log(client.isMobile()); // false
console.log(client.getCanvasPrint()); // data:image/png;base64,...
console.log(client.getFonts()); // "Arial, Calibri, Georgia, ..."
getCustomFingerprint takes variadic arguments — pass whatever signals you want and it hashes them the same way.
Rough edges
The last meaningful commit was a ua-parser-js CVE bump (bump vulnerable ua-parser-js version in the log). Before that: docs. The version is 0.2.1, last released in 2021. This is essentially unmaintained.
The Flash and Java detection modules are dead weight now. Both plugins are extinct in modern browsers, but they're still in the full bundle. You'd always want client.base.min.js.
browserData and fontDetective are module-level globals, not instance properties. If you instantiate two ClientJS objects, the second constructor overwrites the globals the first one set. In practice nobody does that, but it's the kind of thing that would cause confusing bugs if you tried.
The font list (getFonts) iterates 200+ fonts synchronously by DOM measurement. On a slow device or a document with no <body> element yet, this will break. There's no async path.
Modern browsers have increasingly hardened against fingerprinting: Firefox resists canvas fingerprinting, Chrome is degrading navigator.plugins, and font access is restricted in some sandboxed contexts. The fingerprint entropy you get today is lower than it was in 2015 when this library was written. The README doesn't acknowledge this.
Bottom line
If you need browser fingerprinting as a lightweight fallback for cookie-less identification, ClientJS is usable and easy to integrate. Just pull in the base bundle, understand that it's not actively maintained, and don't count on stability against modern browser anti-fingerprinting measures.
