Every anti-detect guide has the same WebGL section. Hook getParameter, watch for UNMASKED_VENDOR_WEBGL and UNMASKED_RENDERER_WEBGL, return a plausible desktop GPU. Two strings, one line each, problem declared solved.
Here is what a page reads on a macOS laptop with an Apple GPU, without touching the renderer string at all:
WEBGL_compressed_texture_pvrtc
WEBGL_compressed_texture_astc
WEBGL_compressed_texture_etc
WEBGL_compressed_texture_etc1
Those four are in getSupportedExtensions(), among 36 total on a WebGL2 context. They are the compressed texture formats of tile-based mobile GPU architectures. Apple’s GPUs are tile-based and expose them. A desktop NVIDIA or AMD part, reached through ANGLE, does not.
So if the renderer string has been rewritten to say ANGLE (NVIDIA, NVIDIA GeForce RTX 4070 Direct3D11 vs_5_0 ps_5_0, D3D11) and the extension list still carries PVRTC and ASTC, the page is looking at an Apple GPU wearing a name tag.
Why the string is the smallest part
getSupportedExtensions() is a list the WebGL implementation builds by asking the driver what it can do. getParameter on the several dozen numeric caps returns limits the driver reports. getShaderPrecisionFormat returns twelve triples describing the numeric behaviour of the shader compiler. None of these is a label. They are all answers from the layer that will actually execute your draw calls.
An implementation that intercepts the two debug-info strings and lets everything else through has produced a browser that says one thing and demonstrates another, in the same API, within microseconds of each other. It is not a subtle inconsistency requiring a reference database of GPUs. Some of these contradictions are structural:
- Compressed texture formats are architecture-bound. PVRTC is PowerVR and Apple lineage. ETC and ETC1 are the OpenGL ES baseline. ASTC is mobile and Apple. S3TC/DXT is the desktop baseline. A GPU claiming to be a desktop discrete part while advertising the mobile set has contradicted itself with no outside information needed.
MAX_TEXTURE_SIZEbelow 8192 on a browser claiming a current desktop GPU is a generational impossibility. Modern parts report 16384 or more.- Shader precision for
HIGH_FLOATin the fragment shader differs between desktop and mobile lineages, and a renderer string cannot change what the compiler reports.
WebGPU says it again, in a different vocabulary
The same fact is available through a second interface, which is what makes it awkward to fix in one place. navigator.gpu gives an adapter, and the adapter carries a feature list:
texture-compression-astc
texture-compression-astc-sliced-3d
texture-compression-etc2
texture-compression-bc <- the desktop one
Those names come from the physical adapter Chrome opened. A persona that has rewritten the WebGL renderer string but not touched WebGPU is offering texture-compression-astc under a claimed desktop vendor, which is the same confession in a different dialect.
And adapter.info carries vendor and architecture fields of its own, which is a third place the same question gets asked. Cross-interface consistency here is a topic of its own; the point for this post is narrower. There are at least three independent surfaces reporting the GPU’s capability profile, they are all populated by the driver, and a fix applied to one of them makes the other two into detectors.
The two directions
The check has a symmetric form, and both halves fire in practice.
Desktop claim, mobile capabilities. A macOS or Android host presenting a Windows or Linux desktop persona. This is the common case, because Windows personas are the default in most tooling and the hosts running that tooling frequently are not Windows.
Mobile claim, no mobile capabilities. An Android persona whose renderer string names an Adreno or Mali part, running on a desktop host whose GPU exposes none of ASTC, ETC or PVRTC. Every Adreno and Mali chip supports those formats. A GPU that claims to be one and cannot name a single one of its formats has failed a check that costs one function call.
Apple is the case that requires care in either direction. Apple Silicon exposes the mobile formats and is a legitimate desktop GPU, so a check that flags “mobile formats present” without first reading the claimed vendor family will mark every honest Mac as a liar. The rule has to be conditioned on what the string says: mobile formats are a contradiction when the claim is NVIDIA, AMD or Intel, and unremarkable when the claim is Apple.
Fixing it properly
The engine-side fix has two halves, and both are worth stating because the first one alone is a trap.
The first half is subtractive: when a persona claims a desktop GPU and provides no explicit extension list, drop the mobile-only formats from what the context reports. That removes the contradiction.
The second half is that subtraction cannot manufacture a plausible GPU. Removing PVRTC from an Apple GPU’s list leaves an Apple GPU’s list minus one entry, which is not an NVIDIA GPU’s list. The rest of the table - the numeric caps, the twelve shader precision triples, the extension set as a whole - still describes the host.
Which is why the approach that survives contact with a real detector is capture and replay: take the whole WebGL surface off a real machine of the type you want to present, store it, and serve it as one coherent unit. Every parameter, every precision triple, the extension list, all from the same physical device. Handled as a whole, it is consistent because it was consistent when captured. Handled piece by piece, every piece you have not thought about is a signal.
There is a subtlety even there. An extension list captured from another machine cannot be served verbatim as truth, because the host still has to be able to execute what it advertises. The workable form is an allow-list: intersect what the captured device supported with what the host really supports, so the reported set is a subset of both. That keeps the browser from promising a format it will fail to decode, at the cost of the intersection being smaller than the capture on hosts that are weaker than the captured device.
The cost of the shortcut
The reason this pattern deserves a name is that it is not specific to WebGL. There is always a label and there is always a table, and the label is always the cheap thing:
| surface | the label | the table underneath |
|---|---|---|
| WebGL | UNMASKED_RENDERER_WEBGL |
~40 numeric caps, 12 precision triples, ~36 extensions |
| WebGPU | adapter.info.vendor |
feature list, ~30 numeric limits |
| Fonts | the enumerable family list | the widths those families measure |
| Codecs | canPlayType strings |
which decoders actually initialise |
| Platform | the User-Agent | which interfaces the binary was compiled with |
In every row, the left column is a value the browser reports and the right column is a value produced by machinery. Anyone can edit the left column. Editing the right column means changing what the machine does, which is a different order of work, and the gap between the two is where detection lives.