When (and When Not) to Add a Three.js Scene to Your Website
3D web visuals can genuinely help a product page or hurt your Core Web Vitals badly — the difference comes down to a handful of implementation decisions.
By NavTech Solution
A 3D hero visual can make a product page feel considerably more polished, and it can also tank your Largest Contentful Paint if it's built without discipline. The technology isn't the risk — the implementation is.
Ask what the 3D scene is actually for
Before writing any Three.js code, be specific about the job the scene is doing. "Looks impressive" isn't a strong enough reason on its own; it needs to either communicate something a flat image can't (a product's actual geometry, an interactive configurator, data that genuinely has three dimensions) or serve a clear brand/attention goal on a page where performance cost is acceptable. If a static image or a CSS animation would communicate the same thing, that's usually the better default — it's cheaper to build, cheaper to maintain, and has zero WebGL performance cost.
Never let it block first paint
The most common mistake: importing Three.js directly into a page component, so its entire bundle ships as part of the initial JavaScript the browser has to parse before anything renders. Three.js is not small. Load it lazily, client-side only, after the rest of the page has already rendered:
"use client";
import dynamic from "next/dynamic";
const HeroScene = dynamic(() => import("./hero-scene"), { ssr: false });
And always render a real fallback underneath — a gradient, a static image, a CSS pattern — not a blank div or a spinner. If the 3D scene never mounts (slow connection, low-end device, JavaScript error), the page should still look intentional, not broken.
Gate it behind device capability and motion preference
Not every device can render a WebGL scene at an acceptable frame rate, and some users have explicitly told their OS they don't want motion:
function canRenderScene() {
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return false;
if (navigator.hardwareConcurrency && navigator.hardwareConcurrency < 4) return false;
if (window.innerWidth < 640) return false;
return true;
}
Respecting prefers-reduced-motion isn't optional politeness — for some users it's the difference between a comfortable page and a genuinely disorienting one. Combine it with a coarse capability check (core count, screen size) so low-power devices get the static fallback instead of a scene that renders at an unusable frame rate.
Keep the scene itself lightweight
A few habits that keep a 3D scene's runtime cost proportional to what it's actually doing:
- Dispose everything on unmount. Geometries, materials, and the renderer itself all hold GPU memory that won't release on its own — clean them up in your component's cleanup function, every time.
- Cap pixel ratio.
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))— rendering at a phone's native 3x pixel density for a decorative hero visual burns GPU budget for a difference most users won't perceive. - Pause when off-screen. Stop the render loop when the tab isn't visible (
document.visibilityState) or the scene has scrolled out of the viewport — there's no reason to keep animating something nobody's looking at. - Prefer vanilla Three.js for simple scenes. React Three Fiber's scene-graph abstraction is genuinely useful for complex, deeply interactive 3D applications, but for a single hero visual it's often extra abstraction over a scene simple enough to manage directly.
The honest tradeoff
Even built well, a 3D scene adds JavaScript weight and GPU cost that a static hero doesn't have. The question isn't "can we make it fast enough to not matter" — with lazy-loading and capability gating, you usually can — it's "does what this scene communicates justify that cost on this specific page." For a product with genuinely three-dimensional value (hardware, spatial data, a configurator), the answer is often yes. For a generic marketing site that wants "a 3D thing" because a competitor has one, a well-designed static or CSS-animated hero usually communicates just as much, for a fraction of the cost.
We build these gated, lazy-loaded, and disposed correctly by default as part of Three.js development, and the same lazy-loading and fallback discipline applies to how we think about Core Web Vitals on every page, 3D or not.