How this site decides between a full WebGL scene, a lighter one, and a static SVG, and why three.js never loads on devices that can't use it.
A WebGL background is a nice first impression on a fast laptop and a bad one on a five-year-old phone. The particle field behind this site, which changes from a halo to a wave to a torus knot to a galaxy as you scroll, has three tiers, and each visitor gets the one their device can run.
The three tiers
- High: the full scene, antialiased, device pixel ratio up to 2, 6,000 particles.
- Low: the same scene with antialiasing off, pixel ratio capped at 1.25 and 2,200 particles.
- Static: an SVG drawing of the hero composition over a CSS gradient, for browsers without WebGL. No three.js download.
That last point matters most. The scene is loaded with next/dynamic and ssr: false, so the roughly 240 KB (gzipped) of three.js, React Three Fiber and Drei is only requested once a device has been assigned a 3D tier:
const WorldScene = dynamic(() => import("./WorldScene"), { ssr: false });
The server always renders the SVG, and it stays visible until the canvas has drawn its first frame, so the hero is never blank while three.js loads.
Choosing a tier
The check runs once, after the page mounts:
- WebGL. If the browser can't create a WebGL context at all, the visitor gets the static tier. That's the only up-front reason to skip 3D.
- Software rendering. If a context only works without
failIfMajorPerformanceCaveat, the browser is rendering on the CPU, so the scene starts on the low tier and lets the performance monitor decide. - Hardware hints.
navigator.deviceMemoryandhardwareConcurrencysplit devices into low and high. A small touch screen is capped at low.
Visitors with prefers-reduced-motion still get the 3D, but a calm version of it: the orb ripples slowly, the particles drift, and nothing follows the pointer.
Adjusting while it runs
Heuristics are guesses, so the scene keeps checking once it's running. Drei's <PerformanceMonitor> watches the frame rate, lowers the pixel ratio when it drops and raises it again when there's headroom. If it keeps flip-flopping, or the browser loses the WebGL context, the site switches to the SVG version.
Two more details:
- There's a single fixed canvas for the whole site, not one per section, so there's only ever one WebGL context and one render loop.
- A small 3D on/off toggle lets visitors override the automatic choice, and the setting is remembered.
The result is the same composition everywhere, with rendering matched to the device.