Vite React项目每次保存新增Canvas元素而非替换的问题排查
问题原因分析
你的核心问题是ThreeJS渲染器的canvas元素重复挂载,且未处理热更新时的资源清理:
- 每次组件热重载(保存文件触发)时,
SceneRenderer组件会重新执行挂载逻辑,创建新的WebGLRenderer实例,并直接往document.body里追加新的canvas元素,旧的canvas和ThreeJS实例没有被销毁或移除。 - 缺少组件卸载时的清理逻辑,导致旧的ThreeJS场景、渲染器、动画帧请求残留,既造成DOM冗余,也可能引发内存泄漏。
修复步骤
1. 将Canvas挂载到组件内部DOM节点(而非直接挂到body)
不要直接操作document.body,而是在组件内创建一个容器,将ThreeJS的canvas挂载到这个容器里,让React的DOM diff机制能正确管理元素。
2. 用useRef保存ThreeJS实例和动画帧ID
避免每次组件渲染时重新创建实例,同时方便在卸载时访问这些实例进行清理。
3. 添加useEffect清理函数
在组件卸载(包括热更新触发的卸载)时,销毁渲染器、移除canvas、取消动画帧请求。
修改后的SceneRenderer.jsx示例代码
import { useEffect, useRef } from 'react'; import * as THREE from 'three'; export default function SceneRenderer() { // 用ref保存容器、渲染器、动画帧ID const containerRef = useRef(null); const rendererRef = useRef(null); const animationIdRef = useRef(null); useEffect(() => { const container = containerRef.current; if (!container) return; // 创建ThreeJS核心实例 const scene = new THREE.Scene(); const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); rendererRef.current = renderer; // 添加立方体和光源 const geometry = new THREE.BoxGeometry(); const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); const cube = new THREE.Mesh(geometry, material); scene.add(cube); camera.position.z = 5; // 将canvas挂载到组件内部容器 container.appendChild(renderer.domElement); // 动画函数 const animate = () => { animationIdRef.current = requestAnimationFrame(animate); cube.rotation.x += 0.01; cube.rotation.y += 0.01; renderer.render(scene, camera); }; animate(); // 窗口 resize 处理 const handleResize = () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }; window.addEventListener('resize', handleResize); // 清理函数:组件卸载时执行 return () => { window.removeEventListener('resize', handleResize); // 取消动画帧 if (animationIdRef.current) { cancelAnimationFrame(animationIdRef.current); } // 销毁渲染器,释放WebGL资源 if (rendererRef.current) { rendererRef.current.dispose(); } // 移除canvas元素 container.innerHTML = ''; }; }, []); // 组件内部的容器节点 return <div ref={containerRef} style={{ width: '100%', height: '100vh' }} />; }
额外说明
- 不要直接操作
document.body:React组件应该管理自己范围内的DOM元素,直接操作body会脱离React的DOM管理机制,导致热更新时无法正确清理旧元素。 - 必须清理ThreeJS资源:WebGLRenderer占用GPU资源,卸载组件时调用
dispose()能释放这些资源,避免内存泄漏;取消动画帧请求能防止旧动画继续运行。
内容的提问来源于stack exchange,提问作者John Cox
相关产品推荐
相关产品推荐

