ClientJS: Understanding Browser Fingerprinting Through a 32-Bit Lens
Hook
Your browser is broadcasting at least 15 unique identifiers right now, and combining them creates a fingerprint that can track you across websites without a single cookie. ClientJS makes this invisible surveillance visible.
Context
Before GDPR and the cookie apocalypse, tracking users across the web was straightforward: drop a cookie, read it later. As privacy regulations tightened and browser vendors added tracking prevention, the digital advertising and fraud detection industries needed alternatives. Browser fingerprinting emerged as the solution—instead of storing an identifier on the user's machine, you generate one from the machine itself.
ClientJS, created by Jack Spirou, demonstrates this technique in pure JavaScript. It queries browser APIs to collect device characteristics like screen resolution, installed fonts, timezone offsets, canvas rendering signatures, and plugin availability. By combining these data points and hashing them into a 32-bit integer, it generates a 'fingerprint' that theoretically identifies unique devices. Beyond tracking, the library exposes individual device attributes, making it useful for analytics dashboards, fraud detection heuristics, and browser compatibility checks. It's a teaching tool as much as a functional library—showing developers exactly how much information their browsers leak passively.
Technical Insight
ClientJS is architecturally straightforward: a collection of getter methods wrapped in a single class that queries browser APIs and system properties. The library doesn't maintain state or make network requests—it's purely a data collector and hasher. The core pattern is simple method chaining that builds a composite fingerprint from individual components.
Here's how you'd use ClientJS to generate a fingerprint and access individual attributes:
const client = new ClientJS();
// Generate the 32-bit fingerprint
const fingerprint = client.getFingerprint();
console.log(fingerprint); // e.g., 2133863245
// Access individual data points
const browserData = {
userAgent: client.getUserAgent(),
screenPrint: client.getScreenPrint(),
colorDepth: client.getColorDepth(),
currentResolution: client.getCurrentResolution(),
availableResolution: client.getAvailableResolution(),
timezone: client.getTimeZone(),
language: client.getLanguage(),
systemLanguage: client.getSystemLanguage(),
plugins: client.getPlugins(),
canvas: client.getCanvasPrint(),
fonts: client.getFonts()
};
console.log(browserData);
The fingerprint generation uses the MurmurHash3 algorithm, a fast non-cryptographic hash function known for good distribution properties. The library concatenates all available data points into a single string, then hashes it to produce the 32-bit integer. This is where the first architectural limitation appears: 32-bit integers only provide about 4.3 billion possible values. Given the birthday paradox, you'll see collisions in populations far smaller than that—around 77,000 users gives you a 50% collision probability.
The canvas fingerprinting technique is particularly interesting. ClientJS renders hidden text with specific fonts and styling to a canvas element, then extracts the pixel data. Because different graphics drivers, operating systems, and hardware configurations render fonts with subtle differences at the sub-pixel level, the resulting image data varies between systems:
// Simplified version of ClientJS canvas fingerprinting
function getCanvasPrint() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const text = 'abcdefghijklmnopqrstuvwxyz0123456789';
ctx.textBaseline = 'top';
ctx.font = "14px 'Arial'";
ctx.textBaseline = 'alphabetic';
ctx.fillStyle = '#f60';
ctx.fillRect(125, 1, 62, 20);
ctx.fillStyle = '#069';
ctx.fillText(text, 2, 15);
ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';
ctx.fillText(text, 4, 17);
return canvas.toDataURL();
}
The library offers modular builds: client.full.js includes Flash and Java detection, client.base.js excludes legacy plugins, and specialized builds isolate Flash or Java detection. This modularity acknowledges that Flash and Java Applets are dead technologies (Flash reached end-of-life in 2020), but maintains backward compatibility for legacy applications.
One clever design decision is exposing both raw data and the fingerprint. Many fingerprinting libraries only return an opaque hash, but ClientJS lets you access individual components. This makes it valuable for fraud detection systems that use decision trees based on specific attributes—for example, flagging transactions where timezone doesn't match credit card billing location, or where browser language conflicts with reported country.
Gotcha
The 32-bit fingerprint is ClientJS's Achilles heel. With only 4.3 billion possible values, collision rates become problematic at scale. A site with 100,000 daily active users has a 68% probability of at least one fingerprint collision. For high-traffic platforms or security-critical applications, this makes ClientJS unsuitable for identifying unique users reliably. You'd need to combine it with server-side heuristics or additional identifiers.
Fingerprint stability is another serious limitation. The fingerprint changes whenever device characteristics change—install a new font, add a browser extension, update your graphics driver, or even resize your browser window (which affects available resolution), and you get a different fingerprint. This makes long-term user tracking unreliable. Studies show browser fingerprints change for 30-40% of users within a week. For session management or short-term fraud detection, this might be acceptable. For building user profiles across months, it's inadequate. The library also lacks privacy safeguards—no consent mechanisms, no disclosure requirements, no consideration for GDPR or CCPA compliance. It's a technical tool that leaves legal and ethical implementation entirely to the developer, which is dangerous given how invasive fingerprinting can be.
Verdict
Use ClientJS if you need basic device fingerprinting for fraud detection heuristics in low-to-medium traffic applications (under 50,000 daily users), or if you specifically need access to individual browser/device characteristics for analytics dashboards or compatibility checks. It's ideal for prototyping fingerprinting systems, educational projects demonstrating browser information leakage, or internal tools where collision risks are acceptable. Skip it for high-security authentication systems where collision rates matter, GDPR-compliant production applications without adding significant consent and disclosure infrastructure, modern applications where the 28-55KB bundle size (including obsolete Flash/Java detection) is wasteful, or any scenario requiring stable long-term user identification. For production fingerprinting, migrate to FingerprintJS which offers better entropy analysis and collision resistance, or for privacy-conscious applications, consider server-side session management with explicit user consent instead of passive fingerprinting.