Shipping a Three.js Background Without Killing Performance
A WebGL hero looks incredible — until it drops a phone to 12fps. Here's the tiering strategy I use to keep a procedural Three.js scene fast everywhere.
The trap
WebGL backgrounds are a performance trap. They look stunning on the M-series MacBook you built them on, then melt a mid-range Android. The fix isn't "less 3D" — it's adaptive 3D.
Detect the device tier first
Before rendering a single frame, classify the device:
const cores = navigator.hardwareConcurrency ?? 4;
const mem = (navigator as any).deviceMemory ?? 4;
const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
const tier =
reduced || mem <= 2 || cores <= 2 ? "low"
: mem <= 4 || cores <= 4 ? "medium"
: "high";Each tier gets a different budget — particle counts, DPR caps, antialiasing, post-processing intensity. The low tier renders zero WebGL: a pure CSS gradient that's indistinguishable at a glance but costs nothing.
Let the renderer self-correct
Even within a tier, frames drop. AdaptiveDpr from drei downscales resolution live when the frame budget is blown, then scales back up when there's headroom.
Never block first paint
The entire canvas is lazy-loaded:
const Scene = dynamic(() => import("./Scene"), { ssr: false });First paint is HTML and text. The 3D layer fades in after. Lighthouse never sees it on the critical path.
Takeaway
Treat the GPU like a budget you spend per device, not a fixed cost. Measure the hardware, render to its tier, and always have a $0 fallback.
Thanks for reading.