A shader backdrop is the easiest way to make a portfolio look expensive and the easiest way to make it feel slow. The gap between those two outcomes is almost entirely budget discipline — knowing what you’re spending per frame, and on what.

The context budget is real and small

A browser will not give you unlimited WebGL contexts. Chrome keeps somewhere around sixteen alive and starts dropping the oldest beyond that, which manifests as canvases further up the page silently going blank as you scroll.

That number is the actual constraint on this kind of work, and it arrives sooner than you’d think: a hero, a six-card grid and a four-frame closing section is eleven contexts on one route. Count them before adding the twelfth. When you need more instances than you have contexts, one context rendering to several targets is the answer, not several contexts.

Fragment shaders scale with pixels, not with elements

The cost of a full-screen effect is width × height × device pixel ratio, and on a modern phone that last term is 3. A backdrop that runs comfortably on a desktop at 1× can be doing nine times the work on a handset with a fraction of the thermal headroom.

Capping the drawing buffer at 2× — or at 1.5× for a soft, low-frequency effect nobody will inspect closely — is usually invisible and routinely halves the frame time. Render at a lower resolution and let the compositor scale it up; a fluid gradient has no high-frequency detail to lose.

Stop when nobody is looking

The single biggest saving is not drawing. An IntersectionObserver that pauses the loop when the canvas leaves the viewport, plus a visibilitychange handler that pauses it when the tab is hidden, together remove most of the battery cost of the whole technique.

This is also why timing an animation off a hidden tab is unreliable: browsers throttle background frames aggressively, so a promise waiting on an animation that never advances may simply never settle. Never make a navigation depend on one.

Degrade honestly

There is no WebGL on a device that’s blocked it, no GPU worth using on some low-end hardware, and no motion at all for a visitor who has asked for none. Each of those has to land on something finished rather than something broken.

The pattern that survives all three: ship the real image in the HTML, mount the effect over it afterwards, and remove the effect rather than the content when any condition fails. The page is complete before the shader compiles, and if it never compiles, nobody finds out.