Boundaries

Your WebGL shaders are not obfuscated

Teams shipping browser graphics tend to assume that turning on a JavaScript protector covers everything in the bundle, shaders included. It does not, and the gap is not an oversight anybody could close. A shader is written in another language and compiled by another compiler, and the browser graphics APIs require your program to hand over the source text intact. That single requirement decides the whole question, and it leads to a configuration recommendation most people find backwards.

A shader is a payload, not a program you shipped

When your renderer starts, it builds a string containing shading language source, passes it to the graphics context, and asks the driver to compile it. The string might be authored inline, imported from a separate file by your bundler, fetched over the network, or assembled at run time from a template with your tuned constants interpolated into it. However it arrives, the thing that reaches the API is text.

A JavaScript obfuscator parses JavaScript and emits JavaScript. When it encounters that text it sees a string literal, exactly like a message or a URL path, and applies whatever string handling you enabled. It has no concept of shading language, does not parse it, and could not usefully transform it even if it tried, because the receiving compiler is strict and the text has to be valid on arrival.

This is the same category distinction that governs stylesheets, JSON payloads and markup templates. The protector transforms the program, and things the program merely carries are carried unchanged.

Reconstructed means readable

The string options do real work on shader literals. Moving strings into a table relocates the literal and leaves an index behind. Encoding respells its characters as escape sequences. Read the output file and you will not see the shader in one obvious block, which is where the false sense of coverage comes from.

But both transforms are reversible by necessity. The driver will not accept an index or an escaped approximation. Before the call happens, your code must produce the original characters in order. So there is a moment, on every page load, when the complete shader source exists as a plain string and is passed as an argument to a known function on a known prototype.

Replacing that function with a wrapper that records its argument and then calls through is a single line in a console, and it captures every shader the page compiles, in authored form, no matter how the literal was stored in the file. The newer graphics API is no different: shader modules are created from a code property on an options object handed to a device method, which is equally interceptable.

This is the general rule that recurs across this site in different clothing. A value that must be reconstructed to be used can be observed at the point of use. It is the same reason an API key cannot be hidden in a bundle, and the same reason a query document travels in the clear.

The tooling is built to show this

Even without touching the prototype, the graphics ecosystem assumes shader visibility. Frame capture libraries that run inside the page record a frame and present every resource that contributed to it, with shaders alongside the uniforms, buffers and draw calls that surround them. Native graphics debuggers do the same from outside the browser. Some browsers expose a debug extension that returns the driver-translated source of a compiled shader.

None of this is adversarial tooling. It exists because graphics programming is close to undebuggable without it. The relevant consequence for you is that the audience able to recover your shaders is not a small population of determined reverse engineers; it is anyone who has done graphics work before.

The configuration advice, which runs the other way

The instinct on discovering all this is to push harder: encode more aggressively, bury the strings further. That spends effort in the one place it cannot pay off. The better move is to take shader source out of the string transforms deliberately.

Shader corpora are commonly tens of kilobytes of text. Routing all of it through the string table inflates the table, adds startup work reconstructing strings whose concealment buys nothing, and makes a compile failure meaningfully harder to diagnose because the text you need to read has been scattered. The reserved strings setting exists for exactly this: it accepts regular expression patterns, and literals matching them stay verbatim, outside both the string table and the encoding pass. It takes up to one hundred patterns of up to five hundred and twelve characters each.

You are not weakening protection by doing this. You are declining a cost for a benefit that was never delivered. Every other string in your program continues to be handled exactly as before.

Where the value actually sits

It is worth challenging the premise that the shader is the asset. A shader describes how a surface responds to light, or how a post-processing pass filters an image. Plenty of that knowledge is public, published in papers and reproduced in open repositories.

What tends to be genuinely hard to reproduce sits around it: the asset pipeline that produced the geometry and textures, the parameter values arrived at through months of tuning, the scene graph and culling strategy, the physics and simulation, the content itself, and the licensing and delivery logic that decides who gets any of it. Almost all of that lives in JavaScript, in your data, and on your servers.

So the protector is working on the right material. Somebody who captures your shaders holds a fragment of a renderer without knowing which uniforms are set when, how passes are ordered, what the buffers contain, or how your asset formats are laid out. Reconstructing a working engine from a protected bundle is laborious in a way that copying a fragment shader is not. That is a real benefit and it is worth stating accurately rather than dressing up as shader concealment.

Constants you interpolate are published

One specific habit deserves attention. Many renderers generate shaders at run time, substituting tuned constants into a template based on quality settings or device capability. That is good engineering. It also means those constants appear in the captured text in final form.

If a value genuinely matters commercially, hiding it in the JavaScript that assembles the string does not help, because interception happens after assembly. The workable options are to deliver it as data behind an authenticated request, to accept that it is visible, or to restructure so the decisive quantity is not a shader input at all. Choosing deliberately among those three is the entire exercise.

The short version

Shading language source is not JavaScript, so a JavaScript obfuscator does not transform it. String options relocate and respell the literal, but the exact text must be reconstructed before the API call, which makes it recoverable in one step at the boundary and trivially visible to ordinary graphics tooling. Exclude shader literals from the string transforms, keep the transforms on for the renderer around them, treat anything interpolated into shader text as published, and put the commercially decisive parts somewhere a capture cannot reach.

Frequently asked questions

Does a JavaScript obfuscator transform shader code?

No, and the reason is definitional rather than a limitation anybody chose. A shader is written in a different language, compiled by a different compiler, and executed on a different processor. To your JavaScript program it is a string: you build it, hold it, and pass it to a graphics API that hands it to the driver. A JavaScript obfuscator parses and rewrites JavaScript. It sees a string literal, applies whatever string handling you enabled, and never has any notion that the contents are a program. The shading language is untouched because it was never in scope.

Then what do the string options actually do to my shaders?

They change where the text sits in your file and how it is spelled there, and nothing else. Moving strings into a table lifts the literal out of the statement that used it and leaves an index behind. Encoding respells characters as escape sequences. Both are reversible by construction, because the graphics API will not accept an escaped or relocated approximation. Your code must hand the driver the exact original text, character for character, or compilation fails. Anything that must be reconstructed to be used can be read at the moment it is reconstructed.

How would somebody actually recover the shader source?

By intercepting the call that delivers it, which takes one line typed into a console. The method that supplies source to a shader object is an ordinary function on a prototype, so replacing it with a wrapper that logs its argument and then calls through captures every shader the page compiles, in original form, regardless of how the string was stored. The same technique applies to the newer graphics API, where shader modules are created from a code property on an options object. This is not an exotic attack; it is the standard way graphics developers debug someone else's renderer.

What about frame capture and debugging tools?

They make it easier still. Browser-based capture libraries and native graphics debuggers exist specifically to record a frame and show you every resource that went into it, shaders included, along with the uniforms and draw calls around them. Some browsers also expose a debug extension that returns the driver-translated shader source. The ecosystem is built on the assumption that this information should be available, because that is what makes graphics work debuggable at all, and no build setting in your JavaScript pipeline opts you out of it.

How much concealment do the browser graphics APIs allow for shader source?

Very little, because the contract is that you supply source text and the implementation compiles it. There is no path that accepts a pre-compiled or encrypted shader binary from page script. That is a deliberate portability decision: the driver on the user's machine has to be the thing that compiles for that hardware. So the honest position is that shader text is public in the same sense that your markup is public, and the design question is what you put in it rather than how to conceal it.

Should I exclude shader source from the string transforms?

In most cases yes, and this is the practical recommendation that surprises people. Shader corpora are frequently tens of kilobytes of text. Pushing all of that through the string table inflates the table, adds startup work to reconstruct strings that offer no protection by being hidden, and makes the output harder to diagnose when a compile fails. The reserved strings setting takes regular expression patterns, and literals matching them stay verbatim and outside both the string table and the encoding pass. It accepts up to one hundred patterns of up to five hundred and twelve characters each.

Does keeping shaders out of the string table weaken my protection?

It does not weaken anything that was working. The protection you get from a string transform is that a reader cannot see the literal by skimming the file, which is worth something for a message, a key name or an endpoint path. It is worth nothing for a value an attacker can capture at the API boundary in one step. You are declining a cost, not a benefit. Everything that genuinely benefits from the transform, which is the rest of your program, continues to receive it.

Where is the real intellectual property in a graphics application?

Usually not in the shader, which is worth saying plainly because it changes what you should spend effort on. A shader expresses how a surface responds to light or how a post-processing pass filters an image. The things that are typically harder to reproduce sit around it: the asset pipeline that produced the geometry and textures, the parameter values arrived at through months of tuning, the scene graph and culling decisions, the physics, and the content itself. Those live in your JavaScript, your data and your servers, and the first of those is exactly what a JavaScript protector does transform.

So what does protecting the JavaScript actually buy a graphics product?

It raises the cost of lifting the engine rather than the effect. Somebody who captures your shaders has a fragment of a renderer with no idea what feeds it: which uniforms are set when, how the passes are ordered, what the buffers contain, how the asset formats are laid out, or how licensing and content delivery are gated. Reconstructing that from a protected bundle is genuinely laborious. That is a real and defensible benefit, and it is a different claim from pretending the shader text is hidden.

What should I do about parameters and constants baked into shaders?

Treat anything you inject into shader source as published, and decide accordingly. Teams often generate shaders at run time by interpolating tuned constants into a template, which is a reasonable engineering pattern and also means those values arrive in the captured text. If a particular constant genuinely matters commercially, the options are to keep it server-side and deliver it as data under an authenticated request, to accept that it is visible, or to restructure so that the interesting value is not a shader input. Hiding it in the JavaScript that builds the string does not survive the interception described above.

Does this apply to WebGPU as much as to WebGL?

Yes, with the same mechanics and slightly different names. The newer API takes shader source as a string on an options object passed to a device method, which is just as interceptable as the older entry point. It does introduce a compiled pipeline concept and a shader module object, but the module is still created from text your page supplies. Nothing in the newer design changes the conclusion, and nothing in it gives page script a way to submit an opaque binary instead.

What is the shortest sensible policy for a graphics team?

Assume shader text is readable and design so that reading it is not the loss. Exclude shader literals from the string transforms so you are not paying for protection you do not receive. Keep the transforms enabled for the JavaScript around them, which is where the reproduction cost actually lives. Keep anything commercially decisive out of shader source and behind an authenticated request. Then test the protected build on real hardware, because graphics code exercises timing and startup order more aggressively than most application code does.

Related reading