The Thermal Wall
We’ve all been there. You spend weeks dialing in the bloom threshold, fine-tuning the chromatic aberration, and adding a subtle film grain overlay that feels just right. You launch the site on your dev machine, it hits a buttery 60fps, and you pat yourself on the back. Then, you open it on an iPhone 13 during a commute. Within ninety seconds, the frame rate craters to 15fps, the screen dims, and the user experiences the digital equivalent of a brick. The thermal throttling has kicked in, and your beautiful post-processing stack is the culprit.
Post-processing in WebGL is often treated like a free lunch. It isn’t. Every pass you add—every extra framebuffer object (FBO) swap—is a tax on the GPU. When we talk about performance, we aren’t just talking about raw frames; we are talking about energy expenditure. Browsers are notoriously heavy, and when you start chaining five or six full-screen shaders, you are asking the mobile GPU to work harder than it was ever designed to.
Understanding the Fill Rate Tax
The biggest bottleneck in post-processing isn't usually the complexity of the math inside your shader. It’s the fill rate. Every time you ping-pong between textures, you are reading from and writing to VRAM. On mobile architectures—which typically use Tile-Based Deferred Rendering (TBDR)—this is lethal. Moving data between the tile buffer and the main memory is the most expensive operation you can perform.
Stop stacking full-resolution passes. It’s a waste. If you are doing a glow effect, do it at a quarter of the resolution. If you’re generating a depth-of-field blur, downsample first. We routinely downsample our render targets by a factor of four for any effect that relies on blurring, like bloom or SSAO.
// Instead of a full-screen quad, sample at lower resolution
const bloomScale = 0.25;
const rtWidth = Math.floor(viewport.width * bloomScale);
const rtHeight = Math.floor(viewport.height * bloomScale);The Case Against Uber-Shaders
A common trap is the 'Uber-Shader'—that massive, branching GLSL file that handles color grading, vignetting, grain, and gamma correction in one go. It feels clean. It feels organized. But it’s a performance nightmare because of the way GPUs handle branching. If you have a thousand pixels and only ten of them trigger that expensive noise function, the GPU will likely execute that path for the entire block to maintain coherency. You aren't saving cycles; you're just making the compiler’s job harder.
Break your pipeline into tiny, focused fragments. Keep them lean. Use gl.drawArrays with a simple triangle that covers the screen rather than a complex mesh. Most importantly, discard what you don't strictly need. If a frame has no motion, don't run the motion blur pass. It sounds obvious, yet it is rarely implemented.
Strategic Optimization Checklist
- Stencil Buffers: Only process pixels that actually changed. If your UI overlay is static, don't run a full-screen post-process over it.
- Texture Precision: Do you really need
FLOATprecision for your bloom buffer? Usually,HALF_FLOATor evenUNSIGNED_BYTEis sufficient for screen-space effects. The memory bandwidth savings are massive. - Early Exit: Use constant buffers to toggle effects off entirely when the browser reports a low-battery state or high thermal pressure.
The goal isn't to make the best looking frame possible; it’s to make the best looking frame that the user's hardware can sustain for an hour without burning their palm.
The Reality of Real-World Constraints
We spent three days chasing a memory leak in a Three.js project only to realize we were re-allocating textures in the render loop. If you’re recreating your render targets every frame, you’re forcing the garbage collector to do overtime while the GPU is already gasping for air. Cache your FBOs. Pre-allocate your buffers. Treat your memory like it’s 1999.
Architecture for mobile isn't about compromise; it’s about respect. Respect the thermal limits of the device. Respect the fact that the user is likely on a 4G connection and expects the page to load without draining their battery by 10% in five minutes. When you strip away the unnecessary passes and optimize your data paths, the result isn't just a faster site—it’s a more reliable one. A smooth 30fps that stays constant is infinitely better than a jittery, inconsistent 60fps that dies the moment the GPU hits 45 degrees Celsius. Stop chasing the frame rate count. Start chasing the thermal signature.
READER ESSAY DISCUSSIONS (0)
Share your architectural perspectives, technical notes, or critique.
No discussions recorded yet.
Be the first to spark the dialogue on this essay.