Key answer: Three.js components are self-contained, reusable units of real-time 3D — a hero scene, a product viewer, a scroll section, a shader background — that expose a clean props API and manage their own scene graph, render loop, and cleanup. A good one lets you drop 3D into a page the way you drop in a button: configure it through props, mount and unmount it safely, and trust it to pause offscreen and fall back for reduced motion. The value is not the geometry; it is the plumbing — lighting, compression, lifecycle, and performance discipline — packaged so you build the site instead of re-engineering the canvas.
Table of contents
- What you'll learn
- What "Three.js components" really means
- Why reusable components matter
- The anatomy of a reusable component
- Plain Three.js vs React Three Fiber
- Building a hero component
- Building a product viewer component
- Lifecycle: mount, pause, and clean up
- Installing a component with an AI assistant
- Performance budget every component shares
- Accessibility and reduced motion
- Trade-offs and limitations
- When to use a 3D component
- When NOT to use one
- Decision matrix: copy-paste vs library vs from scratch
- Expert Notes
- How AETumi approaches it
- GitHub and technical proof
- FAQ
- Related resources
- Conclusion
What you'll learn
- What actually makes a Three.js scene a reusable component rather than a demo
- The anatomy every good component shares: props API, scene graph, render loop, cleanup
- When to reach for React Three Fiber and when plain Three.js is the better tool
- Working Three.js r160 and React Three Fiber code for a hero, a product viewer, and a lifecycle hook
- How an AI coding assistant installs and adapts a component through MCP
- A decision matrix for copying a demo, using a component library, or building from scratch
What "Three.js components" really means
Three.js is a JavaScript library that wraps WebGL — the browser's low-level GPU API — in a workable scene graph of meshes, materials, lights, and cameras. Three.js components take that raw capability and package a specific pattern into a reusable unit: a cinematic hero, an interactive product viewer, a scroll-driven section, an ambient shader background. The distinction that matters is between a demo and a component. A demo renders once, in one file, tied to window globals and hard-coded values. A component exposes a clean props API, owns its own scene graph and render loop, mounts and unmounts without leaking a WebGL context, and behaves predictably on any page you drop it into. That last property — predictable reuse — is the entire point, and it is what most copy-pasted three.js components lack.
Why reusable components matter
Real-time 3D is expensive to get right and cheap to get wrong. The impressive part of any scene — the lighting, the compressed model, the eased controls — takes hours to tune, and the invisible part — pausing when offscreen, cleaning up on unmount, degrading for reduced motion — is what separates a scene that ships from one that janks or leaks. A reusable component amortizes both. Build the pattern once, correctly, and every future page inherits the tuning and the discipline. This is why teams that treat 3D as reusable 3D components ship faster and break less than teams that clone a CodePen per project. The component is the unit of reuse; the props API is the contract.
The anatomy of a reusable component
Every well-built Three.js component, regardless of framework, has the same four parts. A props API turns art direction into configuration — model URL, color, intensity, motion curve — so you adapt a component without editing its internals. A scene graph it constructs and owns: camera, lights, meshes, materials. A render loop it starts and, crucially, stops. And a cleanup path that disposes geometries, materials, textures, and the renderer when the component unmounts, so navigating between routes doesn't accumulate dead WebGL contexts. Miss the fourth part and a single-page app will crash after a dozen route changes; browsers cap live WebGL contexts, and orphaned scenes hold them hostage. A component that gets all four right is one you can trust the way you trust a form input.
Plain Three.js vs React Three Fiber
There are two idioms for authoring three.js components, and both are valid. Plain Three.js gives you an imperative scene you fully control — ideal for a vanilla page, a Web Component, or a framework-agnostic bundle. React Three Fiber (R3F) expresses the same scene graph as declarative JSX, so a mesh is a component and props flow through React's reconciler; lifecycle and cleanup ride on React's own mount/unmount. If your app is already React or Next.js, R3F usually wins because it collapses the render loop and disposal into idioms your team already knows. If you are shipping a portable widget with no framework assumption, plain Three.js keeps the dependency surface small. The patterns below show both.
Building a hero component
A hero is the best first component: self-contained, high-impact, and a clean showcase of a props API. Here it is in React Three Fiber, where the scene graph is JSX and the props are ordinary React props:
// React Three Fiber (@react-three/fiber 8, three r160) — a configurable hero
import { Canvas, useFrame } from '@react-three/fiber';
import { useRef } from 'react';
function Knot({ color = '#2a2a34', speed = 0.4 }) {
const ref = useRef();
useFrame((_, dt) => { ref.current.rotation.y += dt * speed; });
return (
<mesh ref={ref}>
<torusKnotGeometry args={[1, 0.3, 160, 24]} />
<meshStandardMaterial color={color} roughness={0.35} metalness={0.6} />
</mesh>
);
}
export function Hero({ color, speed }) {
return (
<Canvas camera={{ position: [0, 0, 5], fov: 45 }} dpr={[1, 2]}>
<ambientLight intensity={0.15} />
<directionalLight position={[3, 4, 5]} intensity={2.2} />
<directionalLight position={[-4, 2, -3]} intensity={1.0} color="#88aaff" />
<Knot color={color} speed={speed} />
</Canvas>
);
}
Explanation: color and speed are the entire public surface — everything else is encapsulated. dpr={[1, 2]} caps the device pixel ratio so a 4K display doesn't quietly quadruple the work, and useFrame drives rotation frame-rate-independently by multiplying with delta time. Expected behavior: a softly lit knot rotating at a caller-controlled speed, in any color the caller passes. Trade-off: meshStandardMaterial is physically based and looks premium but costs more per pixel than a flat material — perfect for one hero object, wasteful if multiplied across dozens of meshes. This is precisely the kind of unit a curated set of 3D components ships ready to configure.
Building a product viewer component
A product viewer is the highest-value commercial component because it answers questions a photo grid can't. The two mechanics that make it a real component rather than a demo are compressed loading and constrained, damped controls. Here is the loader half in plain Three.js r160:
// three r160, ES modules — load a Draco-compressed glTF for a viewer component
import { GLTFLoader } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/GLTFLoader.js';
import { DRACOLoader } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/DRACOLoader.js';
const draco = new DRACOLoader();
draco.setDecoderPath('https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/libs/draco/');
const loader = new GLTFLoader();
loader.setDRACOLoader(draco);
export function loadProduct(url, onReady) {
loader.load(url, (gltf) => onReady(gltf.scene)); // url is the component's prop
}
Explanation: the model URL is the component's key prop; Draco decoding keeps the download small. Trade-off: Draco shrinks geometry dramatically but adds a decode step and a decoder script to fetch — worth it above a few hundred KB, overkill for a trivial mesh. The controls half constrains the camera so users can't flip the product into a confusing angle:
import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/controls/OrbitControls.js';
export function attachControls(camera, dom) {
const controls = new OrbitControls(camera, dom);
controls.enableDamping = true; // eased, weighty feel
controls.dampingFactor = 0.08;
controls.minDistance = 2; // can't zoom inside the product
controls.maxDistance = 6;
controls.maxPolarAngle = Math.PI * 0.9; // can't flip fully under it
return controls; // caller must call update() each frame
}
Expected behavior: the product rotates with weight and stays within sane bounds. Trade-off: damping requires calling controls.update() every frame, so the loop must run continuously while the viewer is active — which makes the lifecycle discipline in the next section non-negotiable.
Lifecycle: mount, pause, and clean up
The part that separates a component from a demo is lifecycle. A component must stop rendering when it leaves the viewport and dispose its GPU resources when it unmounts. Here is a framework-agnostic pause gate plus disposal:
// Pause offscreen, and dispose on teardown so contexts don't leak
export function createLoop(renderer, scene, camera, controls) {
let running = false;
const io = new IntersectionObserver(([e]) => {
running = e.isIntersecting;
if (running) tick();
});
io.observe(renderer.domElement);
function tick() {
if (!running) return; // stop scheduling frames when hidden
controls?.update();
renderer.render(scene, camera);
requestAnimationFrame(tick);
}
return function dispose() { // call on unmount / route change
io.disconnect();
scene.traverse((o) => { o.geometry?.dispose?.(); o.material?.dispose?.(); });
renderer.dispose();
};
}
Explanation: the IntersectionObserver gates the render loop so an offscreen component costs nothing, and dispose() walks the scene freeing geometries and materials before releasing the renderer. Expected behavior: full smoothness on screen, zero cost off screen, and no accumulating WebGL contexts across navigation. Trade-off: you now own an explicit teardown call — in R3F this is automatic on unmount, which is one more reason a React app benefits from the declarative idiom. This lifecycle is exactly what makes reusable 3D components safe in a single-page app.
Installing a component with an AI assistant
Because these components are packaged units, an AI coding assistant can install and adapt one instead of you wiring it by hand. AETumi exposes its library to agents such as Claude Code, Cursor, and Codex through an MCP server. You register it once in the assistant's MCP configuration:
{
"mcpServers": {
"aetumi": {
"command": "npx",
"args": ["-y", "@aetumi/mcp"],
"env": { "AETUMI_TOKEN": "your-token" }
}
}
}
Explanation: this standard MCP mcpServers block tells the assistant how to launch the AETumi MCP tool. Once connected, you can ask the agent to add a hero or a product viewer, and it scaffolds the component, its props, and the lifecycle wiring into your project. Expected behavior: the agent installs a correct, fast baseline component you then adapt in natural language — swap the model, retune the palette, adjust the motion curve. Trade-off: AI needs direction and review; treat the generated component as a strong first draft you read and test, not a finished feature to merge blind. The MCP workflow turns component reuse into a conversation rather than a copy-paste.
Performance budget every component shares
Every three.js component, whatever its pattern, lives inside one budget: never spend GPU on what the user can't see. The two highest-leverage rules are capping the pixel ratio and pausing offscreen — both shown above. Beyond those, compress models (Draco geometry, KTX2 textures), lazy-initialize the scene so it doesn't block first paint, and reuse a single renderer per canvas rather than spinning up new ones. A component that bakes these in is fast by default; a component that leaves them to the caller is a liability. When you evaluate a library, read for this budget before you look at the visuals — a beautiful component with no offscreen pause is slower than the static section it replaced.
Accessibility and reduced motion
A component is only production-ready if it degrades gracefully. Keep the headline, copy, and CTA as real DOM layered over the canvas — never baked into a texture — so screen readers and crawlers still see the content. And honor the user's motion preference at the component boundary:
// Render a static frame for users who ask for reduced motion
function Knot({ color, speed }) {
const ref = useRef();
const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
useFrame((_, dt) => { if (!reduce) ref.current.rotation.y += dt * speed; });
// ...mesh + material as before
}
Explanation: when reduced motion is requested, the component renders a single static frame instead of animating. Trade-off: you maintain two visual states, but the static one is nearly free and is what a meaningful share of users — and every crawler — will experience. Treat the reduced-motion path as a first-class part of the component's contract, not an afterthought.
Trade-offs and limitations
Components are not free. Every 3D component adds a WebGL runtime cost, a dependency, and a maintenance surface. A library you don't understand can hide the very plumbing you need to debug. Server-side rendering needs care — the canvas is client-only, so hydrate the component lazily and render a placeholder on the server. And a component solves the reuse problem, not the design problem: a hero with no relationship to your brand is still a gimmick, however cleanly it's packaged. Reach for a component when the pattern recurs and the plumbing is the hard part; reach for something simpler when it isn't.
When to use a 3D component
| Situation | Use a component? | Why |
|---|---|---|
| A hero, viewer, or scroll pattern recurs across projects | Yes | Amortize the tuning and lifecycle once |
| Buyers must inspect a physical product | Yes | Product viewer answers questions photos can't |
| Team lacks time to tune lighting and performance | Yes | The plumbing is the hard part, already solved |
| You want AI to scaffold and adapt the 3D | Yes | MCP-installable components fit the workflow |
When NOT to use one
| Situation | Prefer instead | Why |
|---|---|---|
| A static image communicates the same thing | Optimized image + CSS | 3D adds load and complexity for no gain |
| One-off effect you'll never reuse | Inline scene or CSS | A component's overhead isn't justified |
| Strict low-end mobile / data budget | Single image or CSS | WebGL runtime cost isn't warranted |
| Team can't maintain a render pipeline | Template or managed component | Unmaintained 3D rots fast |
Decision matrix: copy-paste vs library vs from scratch
| Need | Copy-paste a demo | Curated component library | Build from scratch |
|---|---|---|---|
| Fastest time-to-ship | Fast but fragile | Fastest | Slowest |
| Clean props API and lifecycle | Rare | Yes | Only if you build it |
| Offscreen pause + cleanup included | Usually missing | Yes | Your responsibility |
| Full control of the render pipeline | Limited | Adaptable | Total |
| Safe in a single-page app | Often leaks contexts | Yes | Depends on your rigor |
| Maintenance burden | High (you own the internals) | Low | High |
Expert Notes
Expert Note — The props API is the component, not the mesh. New teams judge a 3D component by how the scene looks. Judge it instead by its props: can you change the model, color, and motion without opening the file? A component with a tight, well-named props API is one you'll reuse for years; a stunning scene with hard-coded values is a demo you'll rewrite next quarter. Design the contract first.
Expert Note — Cleanup is the difference between a widget and a memory leak. Browsers cap the number of live WebGL contexts, and single-page navigation between routes will hit that cap fast if components don't dispose their scenes. Always ship a teardown path that disposes geometries, materials, textures, and the renderer. In React Three Fiber this rides on unmount for free — one more reason to prefer the declarative idiom inside a React app.
Expert Note — Let AI adapt a correct baseline, not invent from zero. The most reliable way to use an AI coding assistant with 3D is to have it install a vetted component through MCP and then adapt it in plain language. You start from a component that already pauses offscreen, caps pixel ratio, and cleans up — then you retune the art direction. Asking an agent to write a full render pipeline from scratch invites subtle lifecycle bugs; asking it to adapt a known-good component does not.
How AETumi approaches it
AETumi is an AI-native 3D web platform that packages these patterns as production-ready 3D components — heroes, product viewers, scroll modules, and WebGL backgrounds — each shipped with the disciplines this guide argues for already wired in: capped pixel ratio, offscreen pausing, model compression, disposal on unmount, and a reduced-motion fallback. Every component exposes a clean props API so you configure rather than edit internals. Because the platform is AI-native, an assistant like Claude Code can install a component through AETumi MCP and then adapt it to your brand in natural language, so you begin from a correct, fast baseline instead of a blank canvas. The components are buy-once-own-for-life on aetumi.app; the $129 Full Stack tier includes the full component source and the AETumi MCP workflow, so your team owns the code outright rather than renting it.
GitHub and technical proof
Runnable, teaching-grade versions of these components live in the open at github.com/AETumiApp/aetumi-3d-components. The examples load Three.js r160 as ES modules through an import map — no UMD globals — so they match modern module resolution and current APIs like colorSpace and MeshStandardMaterial. You can read a hero or a viewer end to end and see the props API, the offscreen pause, the pixel-ratio cap, and the disposal path in context. The repository is honest about scope: the public examples are single-purpose and teaching-grade, not the full production library, and the README documents performance practice (capping setPixelRatio, lazy init, pausing when hidden, disposing on teardown) rather than making unbenchmarked speed claims. That lets you judge the engineering quality of the pattern before deciding whether the expanded components are worth it.
FAQ
What is a Three.js component? A Three.js component is a self-contained, reusable unit of real-time 3D — such as a hero scene, product viewer, or scroll section — that exposes a clean props API and manages its own scene graph, render loop, and cleanup. Unlike a copy-pasted demo, it can be dropped into any page, configured through props, and mounted or unmounted safely without leaking a WebGL context. The value lives in the packaged plumbing, not the geometry.
How do you make a Three.js scene reusable? Give it a props API so art direction becomes configuration, let it own and construct its own scene graph, gate its render loop so it pauses offscreen, and add a teardown path that disposes geometries, materials, textures, and the renderer on unmount. Those four properties turn a one-off scene into a component you can trust across routes and projects. Missing the cleanup step is the most common cause of single-page apps crashing after repeated navigation.
Should I use React Three Fiber or plain Three.js for components? Use React Three Fiber if your app is already React or Next.js — it expresses the scene graph as declarative JSX and ties lifecycle and cleanup to React's mount/unmount, which removes a whole class of leaks. Use plain Three.js when you're shipping a framework-agnostic widget or Web Component and want to keep the dependency surface minimal. Both produce equally valid components; the choice is about the host environment, not capability.
How do you install a 3D component with an AI coding assistant? Register an MCP server — such as AETumi MCP — in your assistant's configuration, then ask the agent in plain language to add a hero, viewer, or scroll section. The agent scaffolds the component, its props, and its lifecycle wiring, giving you a correct, fast baseline to adapt. Treat the result as a strong first draft you read and test rather than merge unread; AI needs direction and review even when the baseline is sound.
Do Three.js components hurt performance? Only if they skip the budget. A well-built component caps pixel ratio at around 2, pauses its render loop when offscreen, compresses models, and disposes resources on unmount — and stays smooth on a mid-range phone. A component that omits those is slower than the static section it replaced. Read a library for its performance discipline before its visuals, because that discipline is what determines real-world speed.
Related resources
- 3D components — heroes, viewers, and scroll modules as reusable units
- Three.js collection — the scenes behind each component
- 3D scroll modules — function-of-scroll sections
- WebGL techniques — the shader layer underneath
- Three.js website templates — full pages built from these components
Conclusion
Three.js components are worth building because the hard part of 3D is never the geometry — it's the props API, the lifecycle, and the performance budget. Package those once and every page inherits them. Design the contract before the scene, ship cleanup as seriously as visuals, and let an AI assistant adapt a known-good baseline rather than invent one, and you can add real-time 3D to a project with the confidence you'd add any other component.
Want the patterns as ready-to-use building blocks? Explore AETumi's 3D components on aetumi.app — heroes, product viewers, and scroll experiences you can install through MCP, adapt in plain language, and own for life.
More from the AETumi library
Real, production-ready assets — preview the motion, grab the source.

