如何在React Three Fiber中彻底销毁GLB模型及其关联动画与所有资源
Alright, let's tackle this memory leak issue you're facing. The core problem here is that when your Office component unmounts (like when you switch routes), the Three.js resources tied to it—scene objects, skinned meshes, animation mixer, and all their dependencies—aren't being properly cleaned up, leaving them lingering in memory. Here's how to fix it step by step:
1. Properly Track the Animation Mixer with a Ref
First, stop creating a new AnimationMixer on every render. Use a useRef to hold the mixer instance so you can access it later for cleanup:
const mixerRef = useRef(null); // Initialize mixer once when the component mounts useEffect(() => { mixerRef.current = new THREE.AnimationMixer(scene); animations.forEach((clip) => { const action = mixerRef.current.clipAction(clip); action.play(); }); // Cleanup function runs on unmount return () => { // Stop all animations and destroy the mixer mixerRef.current?.stopAllAction(); mixerRef.current?.uncacheRoot(scene); mixerRef.current = null; }; }, [scene, animations]);
2. Prevent Stale useFrame Updates
Your useFrame callback keeps running even after the component unmounts if you don't flag it to stop. Add a mounted state ref to handle this:
const isMountedRef = useRef(true); useFrame((state, delta) => { if (isMountedRef.current && mixerRef.current) { mixerRef.current.update(delta); } }); // Add this to your component's cleanup logic useEffect(() => { return () => { isMountedRef.current = false; }; }, []);
3. Recursively Clean Up All Three.js Resources
Three.js objects like geometries, materials, and textures hold GPU memory that won't be released unless you explicitly call dispose(). Create a helper function to traverse your scene and clean every resource:
const disposeSceneResources = (obj) => { // Clean geometry if (obj.geometry) obj.geometry.dispose(); // Clean materials (handle arrays for multi-material objects) if (obj.material) { Array.isArray(obj.material) ? obj.material.forEach(material => material.dispose()) : obj.material.dispose(); } // Clean textures if present if (obj.texture) obj.texture.dispose(); // Recurse through child objects obj.children.forEach(child => disposeSceneResources(child)); };
Call this function in your component's cleanup useEffect to wipe all resources when the component unmounts:
useEffect(() => { return () => { disposeSceneResources(scene); // Remove the entire scene from the renderer's active scene graph const { scene: threeScene } = useThree.getState(); threeScene.remove(scene); }; }, [scene]);
4. Modified Full Component Code
Putting it all together, here's your updated Office component with complete cleanup logic:
import { useGLTF, useFrame, useThree } from '@react-three/fiber'; import * as THREE from 'three'; import { useEffect, useRef } from 'react'; const Office = (props) => { const { scene, nodes, materials, animations } = useGLTF('/studio.glb'); const mixerRef = useRef(null); const isMountedRef = useRef(true); const group = useRef(); // Camera setup (unchanged) useThree(({ camera }) => { camera.position.set(xPos, yPos, 7); camera.rotation.set(0.1, -0.75, 0.08); camera.fov = 35; camera.updateProjectionMatrix(); }); // Animation mixer initialization + full cleanup useEffect(() => { mixerRef.current = new THREE.AnimationMixer(scene); animations.forEach((clip) => { const action = mixerRef.current.clipAction(clip); action.play(); }); return () => { // Stop animations and clean mixer mixerRef.current?.stopAllAction(); mixerRef.current?.uncacheRoot(scene); mixerRef.current = null; // Mark component as unmounted isMountedRef.current = false; // Dispose all scene resources disposeSceneResources(scene); // Detach scene from renderer const { scene: threeScene } = useThree.getState(); threeScene.remove(scene); }; }, [scene, animations]); // Update mixer only if component is active useFrame((state, delta) => { if (isMountedRef.current && mixerRef.current) { mixerRef.current.update(delta); } }); // Resource disposal helper const disposeSceneResources = (obj) => { if (obj.geometry) obj.geometry.dispose(); if (obj.material) { Array.isArray(obj.material) ? obj.material.forEach(m => m.dispose()) : obj.material.dispose(); } if (obj.texture) obj.texture.dispose(); obj.children.forEach(disposeSceneResources); }; return ( <group ref={group} dispose={null}> <group rotation={[ Math.PI / 2, 0, 0 ]} scale={[ 0.01, 0.01, 0.01 ]}> <group name="Character"> <primitive object={nodes.pasted__Hips} /> <skinnedMesh geometry={nodes.pasted__MocapGuy_Caruncula.geometry} /> {/* Rest of your mesh components */} </group> </group> </group> ); } useGLTF.preload('/studio.glb'); // Preload for smoother navigation
Key Takeaways
- Explicit disposal is non-negotiable for Three.js GPU resources—they won't get garbage collected automatically.
- Stopping the mixer prevents orphaned animation updates and clears cached animation data.
- Detaching the scene from the renderer's active graph ensures no leftover objects are rendered or held in memory.
This setup will fully clean up all 3D resources when the component unmounts, eliminating memory leaks when navigating back and forth with React Router.
内容的提问来源于stack exchange,提问作者Rafael




