Three.js hero sections without wrecking your Core Web Vitals
A WebGL hero can cost you 400 kB and two seconds of LCP — or nothing at all. The difference is entirely in how you load it.
Hieu Nguyen
Founder & lead engineer
Published on · 7 min read
The mistake
Importing @react-three/fiber at the top of a page component pulls Three.js into the initial bundle. The browser now parses hundreds of kilobytes of JavaScript before it can paint your headline. Your LCP element is text, and you have blocked it behind a graphics library.
Load the scene after the text
Render the headline, subtitle and call to action as server-rendered HTML. Put the canvas behind them, mounted by a client component that dynamically imports the scene only after first paint. Until it arrives, show a CSS gradient in the same colours — visitors on slow connections see a finished-looking hero, not a hole.
Respect the device
Cap the device pixel ratio at 1.5. Above that you are rendering four times the pixels for a difference nobody can see on a hero background. Check prefers-reduced-motion and skip the animation loop entirely when it is set — keep the static gradient instead. On navigator.hardwareConcurrency of 4 or less, don't mount the canvas at all.
Stop rendering when nobody is looking
Pause the render loop when the canvas scrolls out of view or the tab loses focus. An idle WebGL loop burns battery for zero benefit and shows up in real-user performance data as jank on unrelated pages.
Keep the geometry cheap
A hero does not need a 50 000-triangle model. Instanced simple shapes, a shader gradient or a particle field read as "advanced 3D" at a fraction of the cost. Reuse one material across instances and never allocate inside the frame loop.
Done this way, the 3D hero costs nothing before first paint and a few dozen kilobytes after it — and the page still scores in the high nineties.
- three.js
- react three fiber
- web performance
- LCP
- webgl