Let's
Build
Greatness
Back to Overview

Mastering Three.js for Marketing Sites

Mastering Three.js for Marketing Sites

There is a specific visual language that separates an exceptional marketing website from a competent one. It is not the copy, not the color palette, not even the photography. It is the presence of something that feels genuinely impossible — an object that bends light, catches reflections, and responds to your touch in a way that feels physical.

This is what Three.js and WebGL make possible on the open web. And in 2025, thanks to the React Three Fiber ecosystem and libraries like Drei, the barrier to entry has dropped dramatically. But mastering the craft — building experiences that are both visually extraordinary and technically sound — still requires deep understanding of how the GPU pipeline works, how the browser loads resources, and how real users interact with these elements on their actual devices.

This is that guide.

Understanding the WebGL Pipeline

Before writing a single line of Three.js, it helps to understand what is actually happening when you render a 3D scene in a browser.

WebGL is a JavaScript API that gives the browser direct access to the GPU. When you create a Three.js scene, you are writing JavaScript that compiles down to GLSL shader code — a C-like language that runs in parallel on thousands of GPU cores simultaneously.

Every visible surface in a 3D scene is defined by two shaders:

  • The vertex shader transforms the 3D coordinates of each point on a mesh into 2D screen coordinates.
  • The fragment shader determines the color of each individual pixel that the mesh covers on the screen.

The MeshTransmissionMaterial from Drei — which we use for the liquid glass effect in the Ruberio hero — runs an extremely complex fragment shader that simulates physical light transmission, refraction, and chromatic aberration. It reads from a framebuffer texture of the scene behind the mesh to calculate what light would actually pass through a glass object at that point. This is why it looks so convincing — and why it is so computationally expensive.

The Performance Challenge: Numbers That Matter

Let us be honest about the cost. A naive Three.js implementation on a modern marketing site adds:

  • ~600KB of JavaScript for Three.js core
  • ~200KB for @react-three/drei (more if you import the entire library)
  • ~50KB for @react-three/fiber

That is roughly 850KB of JavaScript before accounting for GLTF model files, HDR environment maps, or custom shader code. On a fast fiber connection, this downloads in under a second. On a 4G mobile connection in a rural area, it can take 5-8 seconds. This will fail your LCP measurement and penalize your Google Search ranking.

The goal is not to avoid Three.js. The goal is to load it intelligently.

Strategy 1: Lazy Loading with next/dynamic

In Next.js, next/dynamic allows you to defer the loading of a component — and all its dependencies — until the component is actually needed.

const HeroCanvas = dynamic(
  () => import('./HeroCanvas'),
  { ssr: false }
);

The ssr: false flag prevents Next.js from attempting to render the Three.js canvas on the server (which would fail, since WebGL requires a browser context). The component is only downloaded and executed when the parent component mounts in the browser.

For maximum LCP optimization, you can go further: wrap the canvas in an IntersectionObserver and only load the Three.js bundle when the canvas element enters the viewport for the first time.

Strategy 2: Reduce Polygon Count Aggressively

Most Three.js demos use polygon counts that look impressive in screenshots but are completely unnecessary for a rotational background element. The default TorusKnot geometry with arguments [1.5, 0.5, 256, 64] generates approximately 65,000 triangles. Reducing this to [1.5, 0.5, 128, 32] brings the count to around 16,000 — a 75% reduction with no visible difference when the mesh is covered by a transmission material that blurs all geometric edges.

The same principle applies to sphere and icosahedron geometries used for blob effects. A Sphere with [1.8, 32, 32] looks identical to [1.8, 16, 16] at typical viewport sizes and rendering distances, with half the vertex data.

Strategy 3: Control the Render Loop

By default, React Three Fiber renders the scene on every browser frame — up to 144 times per second on high-refresh-rate displays. For an animated scene with a spinning object and mouse-tracking, this is fine when the element is in view. But when the user has scrolled past the hero, rendering continues in the background, consuming GPU cycles unnecessarily.

The frameloop="demand" prop on the Canvas component switches to on-demand rendering. The scene only re-renders when you explicitly call state.invalidate(). Combined with an IntersectionObserver, this completely eliminates off-screen GPU load.

<Canvas frameloop="demand" camera={{ position: [0, 0, 5] }}>
  <AbstractShape isVisible={heroIsInViewport} />
</Canvas>

Inside the useFrame hook in your scene component, you call state.invalidate() only when the element is visible — driving animation when needed, going completely idle when not.

Strategy 4: Manage the DPR

Device Pixel Ratio (DPR) determines how many actual pixels the GPU renders per logical CSS pixel. On an iPhone 15 Pro, the DPR is 3 — meaning a 390×844 CSS canvas requires the GPU to render 1,170×2,532 physical pixels. For a complex transmission shader, this multiplies the rendering work by 9x compared to a DPR of 1.

The dpr prop on the Canvas component lets you cap this:

<Canvas dpr={[1, 1.5]} frameloop="demand">

This tells React Three Fiber to never exceed 1.5x DPR, regardless of the device's native DPR. The visual difference is imperceptible for abstract 3D shapes. The performance improvement on high-DPR mobile devices is dramatic.

Strategy 5: Optimize MeshTransmissionMaterial

For refraction materials, the resolution prop controls the size of the internal framebuffer used to calculate what is visible through the glass. A resolution={1024} creates a 1024×1024 texture map that is updated every frame. Reducing to resolution={256} maintains the visual effect while cutting memory usage and GPU fill rate by 93%.

<MeshTransmissionMaterial
  resolution={256}
  backside={true}
  thickness={2}
  chromaticAberration={0.06}
  temporalDistortion={0}
/>

Note the temporalDistortion={0} setting. The temporal distortion effect creates subtle flickering by blending the current and previous frame's refraction data. It looks beautiful in motion but requires the scene to render continuously — defeating the frameloop="demand" optimization. Setting it to 0 gives you a cleaner, more stable refraction effect at zero extra cost.

Practical Architecture: How We Structure 3D Components at Ruberio

Our approach separates concerns into three files:

  1. Scene.tsx — The actual Three.js objects, materials, and useFrame logic. This file imports from three and @react-three/drei. It accepts props like isVisible and shape.

  2. HeroCanvas.tsx — The <Canvas> wrapper with camera, lighting, and Environment. This is the file that gets loaded lazily via next/dynamic. It imports Scene.tsx.

  3. Hero.tsx — The React component visible to the rest of the application. It contains the IntersectionObserver logic, the text content, and a dynamic import of HeroCanvas. This file has zero Three.js dependencies.

This architecture means that the hero text renders on the server as static HTML with zero JavaScript dependency on Three.js. The 3D canvas loads in the background after the page is interactive. Lighthouse measures LCP against the text — not the canvas — and the score is consistently excellent.

Browser Support and Fallbacks

WebGL 2 is now supported in 97% of browsers globally, including all modern mobile browsers. However, some older Android devices and low-power tablets may not support the specific extensions required by transmission materials.

Always include a graceful fallback. The simplest approach is to check for WebGL support on mount and substitute a high-quality CSS gradient or static image if it is unavailable. The experience degrades gracefully without breaking.

Conclusion

Three.js is one of the most powerful tools available for creating memorable web experiences. But like any powerful tool, it demands respect. Load it lazily. Reduce geometry. Control the render loop. Tune the DPR. Optimize your materials.

When all of these strategies work in concert, you can ship a WebGL hero that scores 95+ on Lighthouse, loads in under 1.5 seconds on 4G, and still takes your visitor's breath away the moment it appears on screen.

That is the standard we hold ourselves to. It is achievable. It just takes discipline.