FingerprintJS is Not Enough: How to Track the Silicon, Not the Browser
Originally published on Medium - read it there ↗
Why FingerprintJS fails at device tracking (and how to fix it)
If you’ve ever integrated the open-source version of FingerprintJS, you’ve likely hit a wall. You open your site in Chrome, get an ID, then open it in Safari on the same MacBook, and - to your frustration - the IDs don’t match.
For a fraud prevention system or a “remember this device” feature, this is a dealbreaker.
The problem isn’t that the library is broken; it’s that it’s doing exactly what it was designed to do: fingerprint the browser. To track a device, we have to dig deeper into the hardware. Here is the blueprint for building a cross-browser identifier in 2026.
The Core Problem: Browser Engines vs. Hardware
Browsers are like filters. Chrome (Blink), Firefox (Gecko), and Safari (WebKit) all “paint” the web differently. They handle fonts, anti-aliasing, and JavaScript execution in unique ways.
If your fingerprinting script includes things like User-Agent strings, Plugin lists, or Canvas text rendering, your ID will break the moment the user switches browsers or updates their software.
To get a stable Device ID, we must ignore the “Software Layer” and anchor ourselves to the “Hardware Layer.”
The Three Anchors of Hardware Identity
To achieve cross-browser parity, we focus on three signals that usually remain identical regardless of which browser is accessing them:
1. The GPU Renderer (WebGL)
Even if you switch from Chrome to Firefox, your graphics card doesn’t change. By querying the WEBGL_debug_renderer_info, we can extract the specific model of the GPU (e.g., NVIDIA GeForce RTX 4070). This is one of the most stable “anchors” available.
2. Audio Stack Latency
Every sound card and driver combination has a unique way of processing audio. By creating a “silent” AudioContext, we can measure the sample rate and base latency. This hardware-bound signature is remarkably consistent across different browser engines on the same OS.
3. Screen Geometry
While window sizes change, screen.width, screen.height, and screen.colorDepth do not. These reflect the physical monitor or mobile display connected to the device.
The Implementation: A Modern Hardware-Bound Script
Here is a streamlined script that strips away the “noise” and focuses on the “signal.”
async function getDeviceFingerprint() {
const components = {};
// 1. Hardware Concurrency (CPU Cores)
components.cores = navigator.hardwareConcurrency || 'unknown';
// 2. Device Memory (RAM - approximate)
components.memory = navigator.deviceMemory || 'unknown';
// 3. Screen Resolution (The physical display)
components.screen = `${screen.width}x${screen.height}x${screen.colorDepth}`;
// 4. Timezone (Location-based hardware setting)
components.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// 5. GPU Fingerprint (WebGL) - Usually identical across browsers
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (gl) {
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
components.gpuRelative = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : 'unknown';
}
// 6. Audio Stack Entropy
// Different hardware handles sample rates and latency slightly differently
try {
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
components.audioSampleRate = audioCtx.sampleRate;
components.audioLatency = audioCtx.baseLatency || 'unknown';
await audioCtx.close();
} catch (e) {
components.audio = 'blocked';
}
// 7. Combine and Hash (Using SubtleCrypto for a clean SHA-256)
const jsonString = JSON.stringify(components);
const encoder = new TextEncoder();
const data = encoder.encode(jsonString);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
// Convert buffer to hex string
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return {
visitorId: hashHex,
raw: components
};
}
// Usage
getDeviceFingerprint().then(data => {
console.log("Your Cross-Browser Device ID:", data.visitorId);
console.log("Hardware Components:", data.raw);
});
The “Foolproof” Catch: Entropy Collisions
Is this solution 100% foolproof? No. And here is why:
If two people buy the exact same model of the M3 MacBook Pro and live in the same timezone, their hardware signals will be identical. This is called an Entropy Collision.
To move from “High Probability” to “Foolproof,” you must implement Server-Side Identity Resolution:
- Don’t just store the Hash: Send the raw hardware data to your database.
- Use Fuzzy Matching: If a login comes in with the same GPU, Screen, and Audio signature, but the IP address has changed slightly, your system can “link” these sessions to the same physical device.
- The “Login Bridge”: Use a successful login event to “stitch” different browser fingerprints together. Once a user logs in via Chrome and then Firefox, your backend knows those two distinct browser hashes belong to one Device_ID.
Conclusion
In 2026, privacy-focused browsers like Safari and Brave are making it harder to track users. However, by ignoring volatile browser data and focusing on the underlying silicon and sensors, you can create a robust identification system that survives a browser switch.
The Golden Rule: The client-side script collects the “clues,” but your server-side logic makes the “arrest.”