In this Article
WebGL fingerprinting is an identifying technique used widely by anti-bot systems to detect automated activities. As it utilizes hardware-level signals, it is quite difficult to bypass, posing another challenge for those who rely on web scraping for business purposes. However, there are ways to maintain a low profile and gather data – read on to learn more.
What is WebGL: a detailed explanation
WebGL stands for Web Graphics Library, which is a JavaScript API. Browsers use it to display dynamic 2D and 3D graphics without requiring the installation of plugins. To perform these rendering tasks, the API relies heavily on the GPU, or Graphics Processing Unit. Websites nowadays utilize WebGL extensively for visual effects, games, and complex graphical interfaces. Actually, WebGL 2 is now actively overtaking, as it offers more features. Modern browsers, such as Chrome, Edge, and Safari, have 95-99% WebGL 2 support, whereas older and headless browsers still lack it.
As there are hundreds of GPUs available and even models from the same manufacturer differ drastically, the same image would be rendered differently. Additionally, the browser, installed drivers, and even hardware variations, like the way the GPU and the motherboard are connected, would also influence the result. It’s like visiting a home technology shop and seeing several dozen TVs on display, all showing the same ad, yet the pictures are slightly different. This became a basis for WebGL fingerprinting. Note that WebGL fingerprinting only happens when JavaScript is involved. A static HTML page cannot perform it.
WebGL fingerprinting: a step-by-step overview
Roughly, the whole process looks like this:
- A website creates a <canvas> element, which is hidden in a page’s HTML. It initializes the WebGL rendering environment.
- JavaScript uses gl.getParameter() calls to extract GPU characteristics such as vendor and model.
- A fingerprinting script requires a browser to render a test image, and readPixels() is used to extract pixel data.
You can visit BrowserLeaks, click on WebGL Report, and see your unique fingerprint. It looks like this:
Below, you will find an example of a script websites use. You can also use it for testing purposes, such as diagnostics, bug reports, compatibility testing, and more. Remember to run the script on devices you own or strictly after receiving explicit consent. Avoid using the script for tracking or deanonymization. Treat results as sensitive data and don’t share with third parties unless you get permission. DataImpulse does not bear responsibility for illegal use of the script.
Step 1. Launch the WebGL environment
// Lightweight FNV-1a hash for strings/byte arrays (synchronous, simple)
function fnv1aHashFromBytes(bytes) {
let h = 0x811c9dc5 >>> 0;
for (let i = 0; i < bytes.length; i++) {
h ^= bytes[i];
// multiply by FNV prime (mod 2^32)
h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
}
return ('00000000' + (h >>> 0).toString(16)).slice(-8);
}
function fnv1aHashFromString(s) {
const bytes = new Uint8Array(s.length);
for (let i = 0; i < s.length; i++) bytes[i] = s.charCodeAt(i) & 0xff;
return fnv1aHashFromBytes(bytes);
}
// Create an (optionally hidden) canvas and return WebGLRenderingContext / WebGL2RenderingContext
function createWebGLContext(opts = {webgl2Preferred: true, preserveDrawingBuffer: false}) {
const canvas = document.createElement('canvas');
canvas.width = 256; canvas.height = 256;
// optionally keep canvas off-DOM so it stays invisible
canvas.style.display = 'none';
document.body && document.body.appendChild(canvas); // append only if available
let gl = null;
if (opts.webgl2Preferred) {
try { gl = canvas.getContext('webgl2', {preserveDrawingBuffer: opts.preserveDrawingBuffer}); } catch (e) {}
}
if (!gl) {
try { gl = canvas.getContext('webgl', {preserveDrawingBuffer: opts.preserveDrawingBuffer}) || canvas.getContext('experimental-webgl'); } catch (e) {}
}
return {gl, canvas};
}
This piece creates an invisible canvas that runs in the background while you browse and defines its resolution. Also, if WebGL 2 isn’t supported, it will fall gracefully to WebGL.
Step 2. Get hardware info
function getWebGLHardwareInfo(gl) {
const info = {};
// Standard parameters
info.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
info.maxCombinedTextureImageUnits = gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS);
info.maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
info.maxVertexUniformVectors = gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS);
info.maxFragmentUniformVectors = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS);
info.shaderPrecision = {
vertexHighFloat: gl.getShaderPrecisionFormat(gl.VERTEX_SHADER, gl.HIGH_FLOAT),
fragmentHighFloat: gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER, gl.HIGH_FLOAT)
};
// Try to get unmasked renderer/vendor (if browser exposes WEBGL_debug_renderer_info)
const dbg = gl.getExtension('WEBGL_debug_renderer_info');
if (dbg) {
info.unmaskedVendor = gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL);
info.unmaskedRenderer = gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL);
} else {
info.unmaskedVendor = null;
info.unmaskedRenderer = null;
}
// Extensions list (useful fingerprinting signal)
info.extensions = gl.getSupportedExtensions() || [];
// Anisotropy (if available)
const extAniso = gl.getExtension('EXT_texture_filter_anisotropic') ||
gl.getExtension('MOZ_EXT_texture_filter_anisotropic') ||
gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic');
if (extAniso) {
const maxAniso = gl.getParameter(extAniso.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
info.maxAnisotropy = maxAniso;
}
return info;
}
It collects various details such as vendor, model, memory size, maximum texture size, shader uniform limits, supported WebGL extension, and other data about your GPU.
Step 3. Probe shader precisions
function probeShaderPrecisions(gl) {
const precisions = {};
const shaderTypes = [
{type: gl.VERTEX_SHADER, name: 'vertex'},
{type: gl.FRAGMENT_SHADER, name: 'fragment'}
];
const levels = [{p: gl.LOW_FLOAT, name: 'low'}, {p: gl.MEDIUM_FLOAT, name: 'medium'}, {p: gl.HIGH_FLOAT, name: 'high'}];
for (const sh of shaderTypes) {
precisions[sh.name] = {};
for (const lv of levels) {
const pf = gl.getShaderPrecisionFormat(sh.type, lv.p);
precisions[sh.name][lv.name] = {rangeMin: pf.rangeMin, rangeMax: pf.rangeMax, precision: pf.precision};
}
}
return precisions;
}
Websites then ask the GPU what precision levels are supported for floating-point operations and fragment shaders, and record their numeric ranges.
Now that a script is done collecting hardware and software info, it proceeds to the next step: image rendering.
Step 4. Test scene rendering
// Simple shader pair: position-only vertex shader and color-fragment that depends on coords
const VERT = `#version 300 es
in vec2 a_pos;
out vec2 v_uv;
void main() {
v_uv = a_pos * 0.5 + 0.5;
gl_Position = vec4(a_pos, 0.0, 1.0);
}`;
const FRAG = `#version 300 es
precision highp float;
in vec2 v_uv;
out vec4 outColor;
void main() {
// deterministic function of coordinates that will exercise GPU math
float r = sin(v_uv.x * 123.456) * 0.5 + 0.5;
float g = cos(v_uv.y * 78.9) * 0.5 + 0.5;
float b = fract((v_uv.x + v_uv.y) * 9876.54321);
outColor = vec4(r, g, b, 1.0);
}`;
function createProgramWebGL2(gl, vertSrc, fragSrc) {
function compile(src, type) {
const s = gl.createShader(type);
gl.shaderSource(s, src);
gl.compileShader(s);
if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
const err = gl.getShaderInfoLog(s);
gl.deleteShader(s);
throw new Error('Shader compile error: ' + err);
}
return s;
}
const vs = compile(vertSrc, gl.VERTEX_SHADER);
const fs = compile(fragSrc, gl.FRAGMENT_SHADER);
const program = gl.createProgram();
gl.attachShader(program, vs);
gl.attachShader(program, fs);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
const err = gl.getProgramInfoLog(program);
throw new Error('Program link error: ' + err);
}
gl.deleteShader(vs);
gl.deleteShader(fs);
return program;
}
function renderTestAndHash(gl, canvas) {
const isWebGL2 = typeof WebGL2RenderingContext !== 'undefined' && gl instanceof WebGL2RenderingContext;
if (!isWebGL2) {
// For WebGL1 we'd need slightly different shaders (no #version, use attribute/varying)
// but the concept is the same. For clarity this example uses WebGL2 if available.
console.warn('Example uses WebGL2 for deterministic shader pipeline; falling back may require shader edits.');
}
// Create program & buffer
const prog = createProgramWebGL2(gl, VERT, FRAG);
gl.useProgram(prog);
const posLoc = gl.getAttribLocation(prog, 'a_pos');
const quad = new Float32Array([
-1, -1,
1, -1,
-1, 1,
1, 1
]);
const vb = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vb);
gl.bufferData(gl.ARRAY_BUFFER, quad, gl.STATIC_DRAW);
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
// Render to the canvas
gl.viewport(0, 0, canvas.width, canvas.height);
gl.clearColor(0,0,0,1);
gl.clear(gl.COLOR_BUFFER_BIT);
const t0 = performance.now();
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
gl.finish(); // ensure rendering completes
const t1 = performance.now();
const renderTime = t1 - t0; // render timing in ms
// Read pixels
const px = new Uint8Array(canvas.width * canvas.height * 4);
gl.readPixels(0, 0, canvas.width, canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, px);
const pixelHash = fnv1aHashFromBytes(px);
// Clean up (optional)
gl.deleteBuffer(vb);
gl.deleteProgram(prog);
return {pixelHash, renderTime};
}
The script asks your browser to display a 2D or 3D scene that may contain specific shades, gradients, or effects. It then reads pixel bytes and learns about pixel colors, image texture, shading, and gradients, computes a hash of those values, and measures the rendering time.
Step 5. Combining data in a fingerprint
function collectWebGLFingerprint(opts = {}) {
const {gl, canvas} = createWebGLContext({webgl2Preferred: true, preserveDrawingBuffer: true});
if (!gl) throw new Error('No WebGL available');
const hw = getWebGLHardwareInfo(gl);
const precisions = probeShaderPrecisions(gl);
// optional: probe more capabilities
const supportsFloatTextures = !!gl.getExtension('OES_texture_float') || !!gl.getExtension('EXT_color_buffer_float');
// Render test and get pixel hash + timing
const {pixelHash, renderTime} = renderTestAndHash(gl, canvas);
// Combine into a fingerprint string
const parts = [
navigator.userAgent || '',
hw.unmaskedVendor || '',
hw.unmaskedRenderer || '',
hw.maxTextureSize,
hw.maxCombinedTextureImageUnits,
hw.maxVertexAttribs,
supportsFloatTextures ? 'floatTex' : '',
hw.extensions ? hw.extensions.join(',') : '',
JSON.stringify(precisions),
pixelHash,
String(Math.round(renderTime)) // include rounded render time
];
const raw = parts.join('|');
const fingerprint = fnv1aHashFromString(raw);
// Return a readable object
return {
fingerprint,
raw,
components: {
ua: navigator.userAgent,
hardware: hw,
precisions,
pixelHash,
renderTime
}
};
}
// Example usage:
try {
const fp = collectWebGLFingerprint();
console.log('WebGL fingerprint:', fp.fingerprint);
console.log('Components:', fp.components);
} catch (e) {
console.error('Could not collect WebGL fingerprint:', e);
}
Finally, the script calls all previous steps, combines all the obtained data into a single long string, and hashes it again to create a more compact and usable fingerprint ID.
This entire process takes place in milliseconds and remains invisible to you. After collecting your fingerprints, websites then store them and compare them against internal databases or send them to anti-bot services for analysis. If a fingerprint matches a known bot or suspicious user, you risk receiving a ban.
WebGL spoofing challenges
There are several reasons why bypassing WebGL fingerprinting may be tricky.
- Differences in GPU rendering behavior may be invisible to human eyes, yet they are obvious for websites, and they stay consistent across pages. Clearing cookies or using incognito mode isn’t helpful.
- The rendering relies on data coming from physical hardware, which is out of reach for JavaScript modifications and browser extensions.
- You can change WebGL parameters each time you load a page; however, if you claim that your GPU is Intel, but it renders like NVIDIA, websites will detect the inconsistency, and this is enough to trigger anti-bot systems.
- The majority of modern detection systems will catch attempts to tamper with the WebGL API.
Does it mean there is no way to get over WebGL fingerprinting? No, there are several techniques that actually work. However, first things first.
Is WebGL fingerprint spoofing legal?
You have a right to protect your privacy, so it is wrong to say that spoofing a WebGL fingerprint is illegal. It’s also used for testing purposes. However, relying on spoofing to commit fraud or violate websites’ Terms of Service is against the law. You should always read the ToS and avoid any illegal actions that may harm other parties’ privacy. The further information is provided solely for educational purposes. DataImpulse doesn’t encourage you to take any actions and doesn’t bear responsibility for possible outcomes.
Proven ways to spoof WebGL fingerprints
- Use headless browsers with additional libraries
Popular tools like Playwright or Selenium, backed up by tools like Playwright Stealth or Undetected Chrome Driver, can slightly modify parameters such as screen size and others. However, you need to be careful so that fake variables and rendered images match each other. This method won’t work with large-scale projects or when attempting to scrape websites using the latest bot detection algorithm. The same goes for browser extensions like WebGL Fingerprint Defender or CanvasBlocker. They intercept WebGL API calls and return altered data, but the rendered image could still mismatch it.
- Opt for GPU virtualization
You can make the browser’s GPU and/or drivers look different from a real physical GPU. There are several approaches you can adopt, for example, replace a real GPU with a software renderer like Switch Shader or use virtual machines with various GPU drivers. On the other hand, if your budget is tight or you lack strong technical skills, think twice before adopting those methods. Additionally, scaling your scraping projects can be a challenging task.
- Request static APIs
This method works if you scrape public data and the target website provides a public or hidden API that returns JSON. On protected dynamic websites, that’s not an option, but in other cases, it’s an attention-worthy choice, as API endpoints are generally less protected.
- Disable WebGL
Disabling WebGL cuts off all the primary WebGL fingerprinting signals (vendor/renderer, extensions, precision, pixel hashes, render timings).
How to disable WebGL
Chrome
Method 1 – via Chrome Flags:
In the address bar, type chrome://flags, press Enter, and select “Disable WebGL” or –disable-webgl. Set the flag to “Disabled”. To activate changes, relaunch Chrome.
Method 2 – via the command line:
Right-click the Google Chrome icon and go to “Properties”. Search for the “Target” line. It should end with something like chrome.exe. At the very end of the line, add a space, and type –disable-webgl. Click “Apply” >”OK”.
Firefox
In the address bar, type about:config and press Enter. If a warning appears, agree to it. Look for webgl.disable. Double-click and set its value to true.
Safari
Go to Safari’s preferences, open the “Advanced” tab, and select “Develop” (if available). Navigate to Experimental Features and find the option for WebGL via Metal. Uncheck the box and restart the browser.
Be cautious, though, as many websites may fail to function correctly with WebGL disabled.
Exploiting WebGL fingerprinting vulnerabilities
You can use some little tricks. For example, scripts often cache results and reuse them instead of recalculating from scratch. You can detect it and either change your environment before the cache is reused (to prevent matching you to a previously recorded identity) or make a site to recalculate a value.
Detector:
// Helper: run the fingerprinting routine and return {time, result}
async function runFingerprintTrial(triggerFn) {
const t0 = performance.now();
const result = await triggerFn(); // should return fingerprint/hash if available
const time = performance.now() - t0;
return { time, result };
}
async function detectCachingRobust(triggerFingerprinting, trials = 5) {
const runs = [];
for (let i = 0; i < trials; i++) {
// small delay to reduce micro-scheduling bias
if (i > 0) await new Promise(r => setTimeout(r, 30));
runs.push(await runFingerprintTrial(triggerFingerprinting));
}
// compute median time
const times = runs.map(r => r.time).sort((a,b) => a-b);
const medianTime = times[Math.floor(times.length/2)];
// compare first vs median of later runs
const firstTime = runs[0].time;
const laterMedian = times.slice(1).length ? times.slice(1)[Math.floor((times.length-1)/2)] : medianTime;
// check result stability (if trigger returns a fingerprint/hash)
const results = runs.map(r => r.result);
const allSame = results.every(x => x === results[0]);
// simple heuristic: if first run is much slower *and* results identical, likely cached
const speedRatio = firstTime / (laterMedian || 1);
const likelyCached = (speedRatio > 5) && allSame;
return {
runs,
medianTime,
firstTime,
laterMedian,
speedRatio,
results,
allSame,
likelyCached
};
}
Force a fresh environment:
function newWebGLContext(w=256,h=256) {
const c = document.createElement('canvas');
c.width = w; c.height = h;
const gl = c.getContext('webgl2') || c.getContext('webgl') || c.getContext('experimental-webgl');
return {c, gl};
}
Force recalculation:
localStorage.removeItem('fp_token'); // only if you can identify it
// or flush likely storage (be careful: may break site)
localStorage.clear();
indexedDB.deleteDatabase('site-db-name'); // needs precise names
Another approach is to detect when a page adds a fingerprinting script and run a spoofing script beforehand. Be aware, though, that it is easily detectable and may disrupt page behavior; this method is more suitable for testing or defensive purposes.
// Robust script interception (best run at document_start)
(function() {
if (window.__scriptInterceptorInstalled) return;
window.__scriptInterceptorInstalled = true;
// Your spoofing routine — define this to patch WebGL or other APIs.
function injectSpoofing() {
try {
// Example: block debug info (replace with your real routine)
const origGetExt = WebGLRenderingContext.prototype.getExtension;
WebGLRenderingContext.prototype.getExtension = function(name) {
if (name === 'WEBGL_debug_renderer_info') return null;
return origGetExt.call(this, name);
};
// mark that spoofing ran
window.__spoofingInjected = true;
} catch (e) {
// swallow errors — don't break page
console.error('injectSpoofing error', e);
}
}
// Helper: decide whether a script looks like fingerprinting (tweak to match)
function looksLikeFingerprintScript(srcOrContent) {
if (!srcOrContent) return false;
const s = String(srcOrContent).toLowerCase();
return s.includes('fingerprint') || s.includes('fingerprinting') || s.includes('fingerprintjs') || s.includes('fingerprint2');
}
// 1) Hook HTMLScriptElement.prototype.src property (setter)
try {
const proto = HTMLScriptElement.prototype;
const srcDesc = Object.getOwnPropertyDescriptor(proto, 'src');
if (srcDesc && srcDesc.configurable) {
Object.defineProperty(proto, 'src', {
configurable: true,
enumerable: srcDesc.enumerable,
get: function() { return srcDesc.get.call(this); },
set: function(val) {
try {
// If the script's src looks suspicious, inject before setting
if (looksLikeFingerprintScript(val) && !window.__spoofingInjected) {
injectSpoofing();
}
} catch (e) { /* ignore */ }
return srcDesc.set.call(this, val);
}
});
}
} catch (e) {
// non-fatal
}
// 2) Hook setAttribute on script elements (covers element.setAttribute('src', ...))
try {
const origSetAttr = HTMLScriptElement.prototype.setAttribute;
HTMLScriptElement.prototype.setAttribute = function(name, value) {
try {
if ((name + '').toLowerCase() === 'src' && looksLikeFingerprintScript(value) && !window.__spoofingInjected) {
injectSpoofing();
}
} catch (e) {}
return origSetAttr.call(this, name, value);
};
} catch (e) {}
// 3) As a fallback, monitor append/insert to catch scripts that already had src set
try {
const origAppend = Node.prototype.appendChild;
Node.prototype.appendChild = function(node) {
try {
if (node && node.tagName && node.tagName.toLowerCase() === 'script') {
const src = node.src || node.getAttribute && node.getAttribute('src');
if (looksLikeFingerprintScript(src) && !window.__spoofingInjected) {
injectSpoofing();
}
}
} catch (e) {}
return origAppend.call(this, node);
};
const origInsertBefore = Node.prototype.insertBefore;
Node.prototype.insertBefore = function(node, ref) {
try {
if (node && node.tagName && node.tagName.toLowerCase() === 'script') {
const src = node.src || node.getAttribute && node.getAttribute('src');
if (looksLikeFingerprintScript(src) && !window.__spoofingInjected) {
injectSpoofing();
}
}
} catch (e) {}
return origInsertBefore.call(this, node, ref);
};
} catch (e) {}
// 4) Small DOM observer as an extra safety net (lightweight)
try {
const obs = new MutationObserver((mutations) => {
for (const m of mutations) {
for (const n of m.addedNodes || []) {
try {
if (n && n.tagName && n.tagName.toLowerCase() === 'script') {
const src = n.src || (n.getAttribute && n.getAttribute('src')) || n.textContent;
if (looksLikeFingerprintScript(src) && !window.__spoofingInjected) {
injectSpoofing();
obs.disconnect();
return;
}
}
} catch (e) {}
}
}
});
obs.observe(document, { childList: true, subtree: true });
} catch (e) {}
// If the page already has script tags at start, scan them now
try {
const scripts = document.getElementsByTagName('script');
for (let i = 0; i < scripts.length; i++) {
const s = scripts[i];
const src = s.src || s.getAttribute && s.getAttribute('src') || s.textContent;
if (looksLikeFingerprintScript(src) && !window.__spoofingInjected) {
injectSpoofing();
break;
}
}
} catch (e) {}
})();
Also, WebGL environments from different pages cannot see each other. You can create isolated contexts to prevent websites from matching your fingerprints across pages.
WebGL vs Canvas
WebGL fingerprinting and Canvas fingerprinting are often used interchangeably, and it’s true that they are closely related. However, they are not 100% the same.
Canvas fingerprinting analyzes how browsers render 2D graphics using HTML5 Canvas. The rendering results mainly depend on the browser’s graphics stack and the rendering engine. As a result, it is easier to spoof.
WebGL fingerprinting analyzes how a browser renders both 2D and 3D graphics. Display results depend on hardware characteristics, making this method more informative and challenging to bypass.
Summary
Now that you know about WebGL fingerprinting spoofing challenges, you may think you were better off ten minutes ago, when you were still clueless. However, your goal isn’t to be invisible – you just have to give a “regular user” impression. For that, your fingerprint needs to look realistic, be consistent within a session, and change from one session to the next. And that’s completely achievable. On the other hand, successful web scraping is only possible when you prevent detection at all levels and employ anti-detect browsers, TLS spoofing solutions, proxies, etc. All those tools and approaches enhance each other, allowing you to stay under the radar. DataImpulse has just a thing for your privacy and anonymity – legally derived, whitelisted proxies for universal scraping needs, along with a 24/7 human support service so you can harvest data without interruption. Click “Try now” or contact us at [email protected] to start.

