Article / React Three Fiber

React Three Fiber or Vanilla Three.js: How I Choose

I ship client work in both. What React Three Fiber actually is, where it genuinely wins, where vanilla Three.js stays the right call, and the decision rule I use at the start of every project.

This question shows up in my messages more than any other technical one: should I learn React Three Fiber or plain Three.js, should we build our project on R3F or vanilla, is R3F slow, is vanilla outdated. The internet mostly answers with tribal loyalty. I ship client work in both, sometimes in the same month, so here is the version with receipts: what each one actually is, where each genuinely wins, and the rule I use to choose.

What R3F actually is

The misconception to clear first: React Three Fiber is not a wrapper library with its own renderer and its own performance characteristics. It is a React reconciler that expresses the Three.js scene graph as JSX. The mesh in your component tree is a real THREE.Mesh, the render loop is a real requestAnimationFrame loop, and the pixels come from the same WebGLRenderer as everyone else's. R3F adds a declarative layer for describing the scene and reacting to state, not a different engine. Which means the question is never which one renders faster. It is which programming model fits this project.

Seeing the two side by side makes the difference concrete. The same object, both ways:

// vanilla: you own the objects and their lifecycle
const mesh = new THREE.Mesh(
  new THREE.BoxGeometry(1, 1, 1),
  new THREE.MeshStandardMaterial({ color: 'tomato' })
);
scene.add(mesh);
// later: scene.remove(mesh); geometry.dispose(); material.dispose();

// R3F: you describe the scene, the reconciler owns the lifecycle
<mesh position={[0, 1, 0]}>
  <boxGeometry args={[1, 1, 1]} />
  <meshStandardMaterial color="tomato" />
</mesh>

That lifecycle line in the vanilla comment is not a joke, it is the most common vanilla bug category I see in code reviews. Three.js does not garbage collect GPU resources: geometries, materials, and textures leak until you dispose them, and long-lived apps with dynamic content bleed memory through every forgotten dispose call. R3F handles mounting and unmounting cleanup automatically, which is a real reliability advantage for application-shaped projects where objects come and go with state. In vanilla you earn that reliability with discipline instead, and the discipline is learnable, but be honest with yourself about whether your team has learned it.

Where R3F wins, with a real project

My Bizsan room planner is an R3F build, and choosing it was not close. That project is an application in the plainest sense: catalog panels, furniture browsers, filters, a guided tour, camera controls, and a 3D scene that all read and write the same state. A rug selected in a DOM panel appears in the scene; an object moved in the scene updates DOM controls. React is excellent at exactly this, and R3F makes the scene a first-class citizen of that state flow instead of a foreign object you synchronize by hand.

The second R3F advantage is the ecosystem, and it is bigger than people outside it realize. Drei alone replaces a folder of helpers I used to maintain: cameras, controls, contact shadows, environment setup, text. The rapier bindings gave the planner physics-based furniture placement in an afternoon. Postprocessing, gestures, state helpers, all maintained, all composable. For application-shaped projects this ecosystem is months of saved work, and pretending otherwise is vanilla purism, not engineering.

Where vanilla wins, with the other projects

My portfolio, the Tenjam configurators, and every choreographed experience I build are vanilla, and that is not close either. These projects are experience-shaped: the frame loop is the product. Preloader choreography, route transitions that hand off between scenes, scroll timelines driving cameras, particles reacting to audio. When the core of the work is imperative sequencing over time, the declarative layer stops helping and starts being a thing you route around, and the escape hatches you would live in, refs everywhere, imperative mutations in useFrame, are just vanilla Three.js wearing a React costume and paying for the wardrobe.

Vanilla also wins on two boring practical axes. Bundle and dependency surface: a React runtime plus reconciler is real weight and real update cadence for a marketing site that has no other React in it. And team fit: when the client's site is WordPress, Shopify, or plain HTML, shipping them a self-contained vanilla bundle, or an embedded viewer, is a cleaner handoff than introducing a React toolchain nobody there maintains.

There is a timing argument too. Three.js is in the middle of its WebGPU and TSL transition, and the new material system and renderer land in vanilla first, with the R3F ecosystem following as the bindings and helpers catch up. None of this blocks R3F work today, but if a project's whole reason to exist is riding the newest rendering path, vanilla keeps you at the front of the line instead of one ecosystem step behind it.

About the performance myth

R3F earned its slow reputation from a real pattern that is not R3F's fault: putting per-frame values in React state. Re-rendering a component tree sixty times a second will bury any project, and beginners do it because state is the React-shaped hammer. The framework's own docs are blunt about the fix, and it is the same discipline vanilla forces anyway: animate through refs and useFrame, mutate objects directly, let React re-render only when the scene's structure changes, not its motion. Written that way, an R3F scene and a vanilla scene produce the same frame times, because they are the same renderer doing the same work. I have moved a stuttering R3F prototype to sixty frames without leaving R3F, purely by moving animation out of state. The library was never the problem.

The day to day differences nobody mentions

Two workflow differences end up mattering more than the architectural debate. Debugging: in R3F, React DevTools shows your scene as a component tree with live props, which makes "why is this mesh here" a browser-panel question instead of a console-log session. Vanilla answers the same question with the three.js devtools extension or a debug UI you build yourself, and honestly, with more console.log than anyone admits. Tuning: the R3F world reaches for leva, the vanilla world for Tweakpane or lil-gui, and they are equally good, but leva binding straight into component state means your debug controls sometimes graduate into production UI for free, which happened twice on the room planner.

And hot reload deserves a mention, because it changes how scenes get built. Vite gives both stacks fast refresh, but R3F preserves component state through edits, so you can tune a material while the camera stays exactly where you parked it. In vanilla, most edits restart the experience from its preloader unless you build state persistence yourself. Small thing, forty times a day.

Mixing them, which is the actual answer sometimes

The choice is also not exclusive within a project. My portfolio pairs a vanilla WebGL experience with plain DOM content above it. The room planner is React throughout. And a pattern I keep using for client embeds is a vanilla scene exposed through a tiny API, mounted inside whatever framework the client has, React included. The seam between DOM interface and canvas experience is a design decision, and both libraries respect it: R3F moves the seam so the scene lives inside the component tree, vanilla keeps the seam at the canvas edge. Neither is wrong. They are different places to put the same boundary.

The rule I actually use

  • If the project is application-shaped, panels, state, catalogs, forms sharing data with the scene: R3F, and take the ecosystem with both hands.
  • If the project is experience-shaped, choreography, transitions, a timeline that is the product: vanilla, and own the loop completely.
  • If the scene embeds into someone else's site: vanilla bundle behind a small API, whatever the host stack is.
  • If the team is React-native and the deadline is short: R3F, because drei and rapier are worth more than architectural purity.
  • If you are learning: learn Three.js concepts first, in either syntax. R3F without understanding the scene graph underneath produces developers who can assemble but cannot debug.

The quiet truth under the whole debate is that the hard parts of this craft, lighting, assets, motion, performance, live below the API layer and transfer completely between the two. Choose per project, not per identity. The chair does not know which syntax placed it.