shader compilation stutter in WebGL — is there actually a solution or do we just warn players?

322 views 9 replies

Been dealing with this for a while on a browser-based game and I'm genuinely not sure there's a clean answer. Every time a new shader program compiles for the first time mid-session, a new enemy type appearing or a new environment chunk loading, there's a noticeable hitch. Sometimes 30ms, sometimes 200ms depending on the GPU. Really kills the feel when your game is supposed to be snappy.

I know the "right" answer is supposed to be shader prewarming: render all your variants invisibly on load so they're already compiled by the time they're needed. I tried this. It works, sort of. But my variant count has grown enough that warming everything upfront adds around 4 seconds to initial load, which just trades one bad experience for a different one.

Things I've actually looked into:

  • KHR_parallel_shader_compile, broadly supported now, lets you kick off compiles async and poll for completion. Helps with load-time stutter but doesn't really help when something triggers a new variant mid-session.
  • Lazy warming during scene transitions: start compiling shaders for the next area behind a loading screen or fade. Works if your game has natural transition points. Mine doesn't always.
  • Reducing variant count via uber-shaders with branching. The tradeoff is real and I've been avoiding it but maybe it's the only honest answer.

WebGPU's createRenderPipelineAsync() is supposedly the real fix here, async pipeline creation that actually returns a Promise and doesn't block the main thread. I haven't fully migrated yet but this is one of the things actively pushing me toward it.

Curious what people are actually shipping with. Is load-time warming just the accepted tax? Are you bucketing variants to only warm the hot paths? Anyone using WebGPU's async pipeline creation in production and finding it meaningfully better, or does it just move the hitch somewhere less visible?

Replying to RogueLark: oh man the keyword variant thing is brutal. we had this exact problem, manifest ...

yeah the combination space is the real problem and static analysis basically can't save you there. we had something similar with a lighting keyword system, manifest looked airtight, prewarming ran clean, and then someone picked up a specific item type under a specific weather condition and boom, first-time stutter.

the thing that finally helped: we added runtime logging that records every unique shader variant string as it actually gets compiled, session by session, and diffed those logs against our prewarming manifest after each playtest. tedious to set up once but it caught combinations that no amount of code review would have found. after a few weeks the manifest was genuinely complete for normal play patterns.

Replying to SolarRunner: Solid approach. One thing to watch: if your materials use shader keyword variant...

oh man the keyword variant thing is brutal. we had this exact problem, manifest looked complete, prewarming loop ran clean in testing, first internal playthrough was fine... until a player picked up a weapon that triggered a glowing emission keyword variant we'd never hit in our prewarming scene. one stutter. filed as 'not reproducible' for two weeks before someone finally connected the dots.

ended up writing a small editor tool that walks every material in the project, enumerates enabled keyword combinations, and spits out the full variant list to the manifest automatically. tedious to build but now at least the manifest is actually exhaustive.

detective finally solving mystery
Replying to NeonCaster: One thing that's helped us: treat the loading screen as a shader compilation win...

Solid approach. One thing to watch: if your materials use shader keyword variants, you need to pre-warm each variant separately, not just each material. We had a manifest that covered all our base materials but missed that several had dedicated depth prepass and shadow caster variants that only compiled when those passes were actually triggered mid-frame. The fix was making the manifest generation step aware of variant permutations and issuing a draw call per variant. Still very much worth the effort, just make sure your manifest is exhaustive or you'll spend time hunting down which specific variant slipped through.

One thing worth trying that doesn't get mentioned enough: the KHR_parallel_shader_compile extension. Check for it with gl.getExtension('KHR_parallel_shader_compile') and if it's available, you can submit shader compilations without blocking and poll completion via COMPLETION_STATUS_KHR. Chrome and Edge both support it. It won't eliminate the problem on unsupported browsers but it cuts the blocking window on the ones that matter most.

Combine it with submitting all your shader programs during a loading phase and doing a dummy draw call for each. In my testing that got me most of the way to stutter-free first encounters. Still not a complete fix, but it's better than just warning players.

Replying to RogueLark: oh man the keyword variant thing is brutal. we had this exact problem, manifest ...

The keyword combination space is genuinely one of those problems that stays invisible until it detonates on you in a playtest. At some point I stopped treating it as a prewarming problem and started treating it as a variant reduction problem: auditing which keyword combinations actually occur at runtime and cutting the ones that don't. Unity's IPreprocessShaders interface lets you hook into the build and strip variants based on custom logic, and if you log which combinations actually get triggered during playtesting you can build a pretty accurate exclusion list over time.

It dropped our variant count enough that the prewarming manifest became manageable. Still tedious as the project grows, but at least it's a known set you can control rather than an exponential one that blows up every time someone adds a new keyword.

Replying to StealthBolt: something that tripped me up with shader pre-warming: loading the material asset...

burned by this exact thing. spent two hours convinced my prewarming loop was working because I could see the materials loading and the editor showed no hitches. first browser test, stutter city. ended up adding an offscreen quad that renders every "prewarm" material for one frame before it can appear in the actual scene. feels wrong to do it that way but it works.

it was working i swear

something that tripped me up with shader pre-warming: loading the material asset does not compile the shader. you have to actually issue a draw call using that material, even if it's off-screen or behind a fullscreen quad. spent two days convinced my pre-warm logic was working when it was doing basically nothing. once I understood that (render the thing, don't just load it) the pre-warming actually started clearing the stutter. seems obvious in retrospect but the docs make it sound like asset loading is enough.

One thing that's helped us: treat the loading screen as a shader compilation window. We maintain a manifest of shaders expected by each level and issue invisible draw calls, offscreen, zero-alpha, doesn't matter, during the load sequence before the player ever sees gameplay. It doesn't catch everything; shaders triggered by edge cases you didn't anticipate still slip through. But it covers the majority of first-occurrence hitches and the ones that remain are rare enough to track down in the next patch cycle rather than ship-blocking.

The manifest maintenance is the annoying part. If you add a new material and forget to update it, you're back to mid-session stutter.

Replying to NimbusWren: yeah the combination space is the real problem and static analysis basically can...

tbh after fighting the manifest approach for a while we gave up trying to enumerate combinations statically and just added a telemetry hook that records shader compile events in production: timestamp, variant keywords, frametime spike. when a real user hits a stutter we can see exactly what triggered it and patch the manifest retroactively. slightly embarrassing to ship but way more effective than trying to reason about the full combination space ahead of time.

this is fine everything is fine

the one thing that actually helped pre-launch: a QA pass specifically designed to trigger rare keyword combos, unusual lighting conditions, obscure powerup states, edge-case weather, run in a browser build with the devtools performance tab open. caught about 30% of the long-tail variants before players did.

Moonjump
Forum Search Shader Sandbox
Sign In Register