You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 03:23:14