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

React FC中THREE.js SideBottleScene实例内存泄漏修复

解决React + Three.js路由切换内存泄漏问题

1. 正确持久化实例引用并绑定卸载清理

用useRef持有SideBottleScene实例,避免因状态更新导致引用丢失,同时确保useEffect的清理函数在组件卸载时触发:

import { useEffect, useRef } from 'react';
import SideBottleScene from './SideBottleScene';

const SideBottle: React.FC = () => {
  // 用useRef持久化实例,TypeScript指定允许null
  const sceneInstanceRef = useRef<SideBottleScene | null>(null);

  useEffect(() => {
    // 创建实例并赋值给ref
    sceneInstanceRef.current = new SideBottleScene(/* 传入容器、配置等参数 */);

    // 清理函数:组件卸载时销毁实例
    return () => {
      if (sceneInstanceRef.current) {
        sceneInstanceRef.current.destroy();
        // 清空引用,帮助GC回收
        sceneInstanceRef.current = null;
      }
    };
  }, []); // 空依赖确保只在挂载/卸载时执行

  return <div ref={/* 绑定Three.js渲染容器的ref */}></div>;
};

export default SideBottle;

2. 完善SideBottleScene的destroy方法(彻底清理Three.js资源)

确保destroy方法完整释放GL上下文、3D资源和事件监听,TypeScript类型问题通过可选属性或类型断言处理:

import * as THREE from 'three';

class SideBottleScene {
  // 将实例属性设为可选类型,允许赋值为null
  private scene?: THREE.Scene;
  private renderer?: THREE.WebGLRenderer;
  private mesh?: THREE.Mesh;
  private keyboardListener?: (e: KeyboardEvent) => void;

  constructor(container: HTMLElement) {
    // 初始化场景、渲染器、模型等逻辑
    this.scene = new THREE.Scene();
    this.renderer = new THREE.WebGLRenderer({ antialias: true });
    container.appendChild(this.renderer.domElement);

    // 示例:绑定键盘事件
    this.keyboardListener = (e) => { /* 事件逻辑 */ };
    window.addEventListener('keydown', this.keyboardListener);
  }

  public destroy(): void {
    // 移除事件监听
    if (this.keyboardListener) {
      window.removeEventListener('keydown', this.keyboardListener);
      this.keyboardListener = undefined;
    }

    // 清理场景中的3D对象
    if (this.scene) {
      // 遍历并销毁所有子对象的几何体、材质
      this.scene.traverse((obj) => {
        if (obj instanceof THREE.Mesh) {
          obj.geometry.dispose();
          // 材质可能是数组或单个对象
          if (Array.isArray(obj.material)) {
            obj.material.forEach(material => material.dispose());
          } else {
            obj.material.dispose();
          }
        }
      });
      // 清空场景
      this.scene.clear();
      this.scene = null;
    }

    // 销毁渲染器并释放GL上下文
    if (this.renderer) {
      // 从DOM中移除canvas元素
      this.renderer.domElement.remove();
      // 释放WebGL资源
      this.renderer.dispose();
      // 强制丢失GL上下文(关键:彻底释放GPU资源)
      (this.renderer as THREE.WebGLRenderer).forceContextLoss();
      this.renderer = null;
    }

    // 清空模型引用
    this.mesh = null;
  }
}

export default SideBottleScene;

3. 验证组件卸载逻辑(确保路由切换时组件被卸载)

如果清理函数仍未执行,检查路由配置:

  • 确认没有使用缓存组件的逻辑(如自定义keep-alive)
  • 可通过在组件中添加监听验证卸载:
    useEffect(() => {
      return () => {
        console.log('SideBottle组件已卸载');
      };
    }, []);
    
    若路由切换时未打印日志,说明组件未被卸载,需调整路由或组件挂载逻辑。

4. 路由切换时强制清理(兜底方案)

如果组件因某些原因未被卸载,可监听路由变化手动触发销毁:

import { useLocation, useHistory } from 'react-router-dom';

const SideBottle: React.FC = () => {
  const location = useLocation();
  const history = useHistory();
  const sceneInstanceRef = useRef<SideBottleScene | null>(null);

  useEffect(() => {
    // 初始化实例...

    // 监听路由变化,当离开当前路由时销毁实例
    const handleRouteChange = () => {
      // 替换为当前组件对应的路由路径
      if (location.pathname !== '/bottle-list' && sceneInstanceRef.current) {
        sceneInstanceRef.current.destroy();
        sceneInstanceRef.current = null;
      }
    };

    const unsubscribe = history.listen(handleRouteChange);

    return () => {
      unsubscribe();
      handleRouteChange();
    };
  }, [location.pathname, history]);

  // ...
};

内容的提问来源于stack exchange,提问作者nok

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 18:00:27