{T}

内存管理

合理的内存管理对于 Three.js 应用的稳定运行至关重要。避免内存泄漏可以确保应用长时间运行而不崩溃,同时优化内存使用可以提升应用性能和用户体验。

概述

Three.js 应用涉及大量 GPU 资源(几何体、纹理、材质等),这些资源需要手动释放,否则会导致内存泄漏。内存管理的核心目标是:

  1. 防止泄漏:确保不再使用的资源被正确释放
  2. 优化使用:合理复用资源,避免重复创建
  3. 监控预警:实时监控内存状态,及时发现问题
  4. 生命周期管理:建立完善的资源生命周期管理体系

内存管理流程

plaintext
┌─────────────┐
│  资源创建   │ ──> 记录资源引用
└──────┬──────┘


┌─────────────┐
│  资源使用   │ ──> 正常使用阶段
└──────┬──────┘


┌─────────────┐
│  资源标记   │ ──> 标记为不再使用
└──────┬──────┘


┌─────────────┐
│  资源释放   │ ──> 调用 dispose() 方法
└──────┬──────┘


┌─────────────┐
│  引用清理   │ ──> 清除所有引用
└─────────────┘

系统架构

内存管理系统架构

plaintext
┌──────────────────────────────────────────────────────────┐
│                  内存管理系统架构                          │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐  │
│  │  资源层      │  │  管理层      │  │  监控层      │  │
│  ├──────────────┤  ├──────────────┤  ├──────────────┤  │
│  │ • 几何体     │  │ • 资源注册   │  │ • 使用统计   │  │
│  │ • 纹理       │  │ • 引用计数   │  │ • 泄漏检测   │  │
│  │ • 材质       │  │ • 自动释放   │  │ • 报警系统   │  │
│  │ • 渲染目标   │  │ • 缓存管理   │  │ • 日志记录   │  │
│  └──────────────┘  └──────────────┘  └──────────────┘  │
│                                                          │
│  ┌────────────────────────────────────────────────┐    │
│  │              生命周期管理器                      │    │
│  ├────────────────────────────────────────────────┤    │
│  │  创建 │ 使用 │ 暂停 │ 恢复 │ 释放              │    │
│  └────────────────────────────────────────────────┘    │
│                                                          │
└──────────────────────────────────────────────────────────┘

资源类型与生命周期

资源类型GPU 内存释放方法生命周期管理难度
BufferGeometrydispose()场景级别
Texturedispose()材质级别
Materialdispose()对象级别
WebGLRenderTargetdispose()功能级别
ShaderMaterialdispose()材质级别

内存指标参考

内存使用标准

平台推荐内存警告阈值危险阈值建议操作
桌面浏览器< 500MB500MB-1GB> 1GB合理范围
移动浏览器< 100MB100MB-200MB> 200MB需要优化
低端移动< 50MB50MB-100MB> 100MB紧急优化

内存增长判断标准

增长趋势判定结果建议操作
稳定(< 5% 波动)正常持续监控
轻微增长(5-10%)关注检查资源释放
明显增长(10-20%)警告排查泄漏
快速增长(> 20%)危险紧急处理

资源数量参考

资源类型低端设备中端设备高端设备
几何体< 50< 200< 1000
纹理< 20< 100< 500
材质< 30< 150< 500
渲染目标< 5< 20< 50

资源释放基础

dispose() 方法

所有 Three.js 资源都实现了 dispose() 方法,这是释放 GPU 资源的标准方式。

javascript
import * as THREE from 'three';
 
// 几何体
geometry.dispose();
 
// 材质
material.dispose();
 
// 纹理
texture.dispose();
 
// 渲染目标
renderTarget.dispose();

完整释放流程

javascript
// 创建对象
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ 
  color: 0x00ff00,
  map: textureLoader.load('texture.jpg')
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
 
// 释放资源
function disposeMesh(mesh) {
  // 从场景移除
  scene.remove(mesh);
  
  // 释放几何体
  if (mesh.geometry) {
    mesh.geometry.dispose();
  }
  
  // 释放材质
  if (mesh.material) {
    if (Array.isArray(mesh.material)) {
      mesh.material.forEach(material => disposeMaterial(material));
    } else {
      disposeMaterial(mesh.material);
    }
  }
}
 
function disposeMaterial(material) {
  // 释放所有纹理
  const textureProperties = [
    'map', 'normalMap', 'roughnessMap', 'metalnessMap',
    'aoMap', 'emissiveMap', 'alphaMap', 'bumpMap',
    'displacementMap', 'envMap', 'lightMap'
  ];
  
  textureProperties.forEach(prop => {
    if (material[prop] && typeof material[prop].dispose === 'function') {
      material[prop].dispose();
    }
  });
  
  // 释放材质
  material.dispose();
}

资源释放配置表

资源类型需要释放关联资源注意事项
BufferGeometry释放后无法使用
Texture图片元素释放 Image 对象
Material所有纹理先释放纹理
Mesh几何体、材质先移除引用
Scene部分子对象遍历释放子对象

资源管理器

基础资源管理器

javascript
class ResourceManager {
  constructor() {
    this.resources = new Map();
    this.textures = new Map();
    this.geometries = new Map();
    this.materials = new Map();
  }
  
  // 添加资源
  add(id, resource, type = 'general') {
    this.resources.set(id, { resource, type, createdAt: Date.now() });
    
    switch (type) {
      case 'texture':
        this.textures.set(id, resource);
        break;
      case 'geometry':
        this.geometries.set(id, resource);
        break;
      case 'material':
        this.materials.set(id, resource);
        break;
    }
    
    return resource;
  }
  
  // 获取资源
  get(id) {
    const item = this.resources.get(id);
    return item ? item.resource : null;
  }
  
  // 检查资源是否存在
  has(id) {
    return this.resources.has(id);
  }
  
  // 释放单个资源
  dispose(id) {
    const item = this.resources.get(id);
    if (!item) return;
    
    if (item.resource.dispose) {
      item.resource.dispose();
    }
    
    this.resources.delete(id);
    this.textures.delete(id);
    this.geometries.delete(id);
    this.materials.delete(id);
  }
  
  // 释放所有资源
  disposeAll() {
    this.resources.forEach(({ resource }) => {
      if (resource.dispose) {
        resource.dispose();
      }
    });
    
    this.resources.clear();
    this.textures.clear();
    this.geometries.clear();
    this.materials.clear();
  }
  
  // 获取资源数量
  getCount() {
    return {
      total: this.resources.size,
      textures: this.textures.size,
      geometries: this.geometries.size,
      materials: this.materials.size
    };
  }
  
  // 获取资源详情
  getDetails() {
    const details = [];
    this.resources.forEach((item, id) => {
      details.push({
        id,
        type: item.type,
        createdAt: new Date(item.createdAt).toISOString(),
        age: Date.now() - item.createdAt
      });
    });
    return details;
  }
}
 
// 使用
const resourceManager = new ResourceManager();
 
// 添加资源
const texture = resourceManager.add(
  'texture1',
  textureLoader.load('texture.jpg'),
  'texture'
);
 
const geometry = resourceManager.add(
  'geometry1',
  new THREE.BoxGeometry(1, 1, 1),
  'geometry'
);
 
// 释放资源
resourceManager.dispose('texture1');
 
// 释放所有
resourceManager.disposeAll();
 
// 获取统计
console.log(resourceManager.getCount());

引用计数资源管理器

javascript
class ReferenceCountedResourceManager {
  constructor() {
    this.resources = new Map();
  }
  
  add(id, resource) {
    if (this.resources.has(id)) {
      const item = this.resources.get(id);
      item.refCount++;
      return item.resource;
    }
    
    this.resources.set(id, {
      resource,
      refCount: 1,
      createdAt: Date.now()
    });
    
    return resource;
  }
  
  get(id) {
    return this.resources.get(id)?.resource;
  }
  
  release(id) {
    const item = this.resources.get(id);
    if (!item) return;
    
    item.refCount--;
    
    if (item.refCount <= 0) {
      if (item.resource.dispose) {
        item.resource.dispose();
      }
      this.resources.delete(id);
    }
  }
  
  getStats() {
    let totalRefs = 0;
    this.resources.forEach(item => {
      totalRefs += item.refCount;
    });
    
    return {
      uniqueResources: this.resources.size,
      totalReferences: totalRefs
    };
  }
}
 
// 使用
const manager = new ReferenceCountedResourceManager();
 
// 多次引用同一资源
const tex1 = manager.add('texture', texture1);  // refCount: 1
const tex2 = manager.add('texture', texture1);  // refCount: 2
 
// 释放引用
manager.release('texture');  // refCount: 1
manager.release('texture');  // refCount: 0, 资源被释放

场景资源管理

javascript
class SceneResourceManager {
  constructor() {
    this.disposables = new Set();
    this.autoDispose = true;
  }
  
  // 跟踪场景中的所有资源
  track(scene) {
    scene.traverse((object) => {
      if (object.geometry) {
        this.disposables.add(object.geometry);
      }
      
      if (object.material) {
        if (Array.isArray(object.material)) {
          object.material.forEach(m => this.trackMaterial(m));
        } else {
          this.trackMaterial(object.material);
        }
      }
    });
  }
  
  trackMaterial(material) {
    this.disposables.add(material);
    
    // 跟踪纹理
    const textureProps = [
      'map', 'normalMap', 'roughnessMap', 'metalnessMap',
      'aoMap', 'emissiveMap', 'alphaMap'
    ];
    
    textureProps.forEach(prop => {
      if (material[prop]) {
        this.disposables.add(material[prop]);
      }
    });
  }
  
  // 释放所有资源
  dispose() {
    this.disposables.forEach(resource => {
      if (resource.dispose) {
        resource.dispose();
      }
    });
    this.disposables.clear();
  }
  
  // 获取统计
  getStats() {
    let geometries = 0;
    let materials = 0;
    let textures = 0;
    let others = 0;
    
    this.disposables.forEach(resource => {
      if (resource.isBufferGeometry) geometries++;
      else if (resource.isMaterial) materials++;
      else if (resource.isTexture) textures++;
      else others++;
    });
    
    return { 
      total: this.disposables.size,
      geometries, 
      materials, 
      textures,
      others 
    };
  }
  
  // 导出资源列表
  exportList() {
    const list = [];
    this.disposables.forEach(resource => {
      list.push({
        type: resource.type || resource.constructor.name,
        uuid: resource.uuid
      });
    });
    return list;
  }
}
 
// 使用
const manager = new SceneResourceManager();
manager.track(scene);
 
console.log('资源统计:', manager.getStats());
console.log('资源列表:', manager.exportList());
 
// 不再需要时
manager.dispose();

内存泄漏检测

常见泄漏原因

javascript
// 1. 未释放事件监听器
window.addEventListener('resize', onResize);
// 需要移除
window.removeEventListener('resize', onResize);
 
// 2. 未释放动画循环
const animationId = requestAnimationFrame(animate);
// 需要取消
cancelAnimationFrame(animationId);
 
// 3. 未释放 Three.js 资源
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshStandardMaterial();
// 需要调用 dispose()
 
// 4. 闭包持有引用
function createClosure() {
  const largeData = new Array(1000000);
  return function() {
    console.log(largeData.length);  // largeData 无法被回收
  };
}
 
// 5. 未清理的对象属性
const scene = new THREE.Scene();
scene.userData.largeArray = new Array(1000000);
// 需要清理
delete scene.userData.largeArray;

内存泄漏检测工具

javascript
class MemoryLeakDetector {
  constructor(options = {}) {
    this.snapshots = [];
    this.warningThreshold = options.warningThreshold || 100 * 1024 * 1024;  // 100 MB
    this.sampleInterval = options.sampleInterval || 1000;
    this.maxSnapshots = options.maxSnapshots || 100;
    this.intervalId = null;
  }
  
  start() {
    this.intervalId = setInterval(() => {
      this.snapshot();
      const warning = this.check();
      if (warning) {
        console.warn('内存警告:', warning);
      }
    }, this.sampleInterval);
  }
  
  stop() {
    if (this.intervalId) {
      clearInterval(this.intervalId);
      this.intervalId = null;
    }
  }
  
  snapshot() {
    if (performance.memory) {
      const memory = {
        timestamp: Date.now(),
        used: performance.memory.usedJSHeapSize,
        total: performance.memory.totalJSHeapSize
      };
      
      this.snapshots.push(memory);
      
      // 保留最近 N 个快照
      if (this.snapshots.length > this.maxSnapshots) {
        this.snapshots.shift();
      }
      
      return memory;
    }
    return null;
  }
  
  check() {
    if (this.snapshots.length < 10) return null;
    
    const recent = this.snapshots.slice(-10);
    const older = this.snapshots.slice(-20, -10);
    
    const recentAvg = recent.reduce((a, b) => a + b.used, 0) / recent.length;
    const olderAvg = older.reduce((a, b) => a + b.used, 0) / older.length;
    
    const growth = (recentAvg - olderAvg) / olderAvg;
    
    if (growth > 0.1) {  // 增长超过 10%
      return {
        type: 'growth',
        message: '检测到内存增长趋势',
        growth: (growth * 100).toFixed(1) + '%',
        recentAvg: (recentAvg / 1024 / 1024).toFixed(2) + ' MB',
        olderAvg: (olderAvg / 1024 / 1024).toFixed(2) + ' MB'
      };
    }
    
    if (recentAvg > this.warningThreshold) {
      return {
        type: 'threshold',
        message: '内存使用过高',
        used: (recentAvg / 1024 / 1024).toFixed(2) + ' MB',
        threshold: (this.warningThreshold / 1024 / 1024).toFixed(2) + ' MB'
      };
    }
    
    return null;
  }
  
  getReport() {
    if (this.snapshots.length === 0) return null;
    
    const latest = this.snapshots[this.snapshots.length - 1];
    const first = this.snapshots[0];
    const growth = latest.used - first.used;
    
    const used = this.snapshots.map(s => s.used);
    
    return {
      current: (latest.used / 1024 / 1024).toFixed(2) + ' MB',
      peak: (Math.max(...used) / 1024 / 1024).toFixed(2) + ' MB',
      min: (Math.min(...used) / 1024 / 1024).toFixed(2) + ' MB',
      average: (used.reduce((a, b) => a + b) / used.length / 1024 / 1024).toFixed(2) + ' MB',
      growth: (growth / 1024 / 1024).toFixed(2) + ' MB',
      snapshots: this.snapshots.length,
      duration: ((latest.timestamp - first.timestamp) / 1000).toFixed(0) + 's'
    };
  }
  
  reset() {
    this.snapshots = [];
  }
}
 
// 使用
const detector = new MemoryLeakDetector({
  warningThreshold: 200 * 1024 * 1024,  // 200 MB
  sampleInterval: 2000
});
 
detector.start();
 
// 定期检查报告
setInterval(() => {
  console.log('内存报告:', detector.getReport());
}, 10000);

对象追踪器

javascript
class ObjectTracker {
  constructor() {
    this.objects = new Map();
    this.autoCleanup = false;
  }
  
  track(object, category = 'general') {
    const id = object.uuid || Math.random().toString(36);
    
    this.objects.set(id, {
      object,
      category,
      createdAt: Date.now(),
      stackTrace: new Error().stack
    });
    
    return id;
  }
  
  untrack(id) {
    this.objects.delete(id);
  }
  
  getByCategory(category) {
    const result = [];
    this.objects.forEach((item, id) => {
      if (item.category === category) {
        result.push({ id, ...item });
      }
    });
    return result;
  }
  
  findLeaks(maxAge = 60000) {  // 默认 1 分钟
    const leaks = [];
    const now = Date.now();
    
    this.objects.forEach((item, id) => {
      const age = now - item.createdAt;
      if (age > maxAge) {
        leaks.push({
          id,
          category: item.category,
          age: (age / 1000).toFixed(0) + 's',
          stackTrace: item.stackTrace
        });
      }
    });
    
    return leaks;
  }
  
  getStats() {
    const stats = {
      total: this.objects.size,
      byCategory: {}
    };
    
    this.objects.forEach(item => {
      stats.byCategory[item.category] = (stats.byCategory[item.category] || 0) + 1;
    });
    
    return stats;
  }
}
 
// 使用
const tracker = new ObjectTracker();
 
// 追踪对象
const mesh = new THREE.Mesh(geometry, material);
tracker.track(mesh, 'mesh');
tracker.track(geometry, 'geometry');
tracker.track(material, 'material');
 
// 定期检查泄漏
setInterval(() => {
  const leaks = tracker.findLeaks(30000);  // 30 秒以上的对象
  if (leaks.length > 0) {
    console.warn('可能的内存泄漏:', leaks);
  }
}, 10000);

纹理内存优化

纹理管理

纹理是内存占用最大的资源之一,需要特别管理。

javascript
class TextureManager {
  constructor(maxSize = 100) {
    this.cache = new Map();
    this.maxSize = maxSize;
    this.lruList = [];
    this.loading = new Map();
  }
  
  async load(url, options = {}) {
    // 检查缓存
    if (this.cache.has(url)) {
      this.updateLRU(url);
      return this.cache.get(url);
    }
    
    // 检查是否正在加载
    if (this.loading.has(url)) {
      return this.loading.get(url);
    }
    
    // 检查缓存大小
    if (this.cache.size >= this.maxSize) {
      this.evict();
    }
    
    // 创建加载 Promise
    const loadPromise = new Promise((resolve, reject) => {
      const loader = new THREE.TextureLoader();
      loader.load(
        url,
        texture => {
          // 应用选项
          if (options.encoding) {
            texture.encoding = options.encoding;
          }
          if (options.anisotropy) {
            texture.anisotropy = options.anisotropy;
          }
          if (options.generateMipmaps !== undefined) {
            texture.generateMipmaps = options.generateMipmaps;
          }
          
          // 添加到缓存
          this.cache.set(url, texture);
          this.lruList.push(url);
          this.loading.delete(url);
          
          resolve(texture);
        },
        undefined,
        error => {
          this.loading.delete(url);
          reject(error);
        }
      );
    });
    
    this.loading.set(url, loadPromise);
    
    return loadPromise;
  }
  
  updateLRU(url) {
    const index = this.lruList.indexOf(url);
    if (index > -1) {
      this.lruList.splice(index, 1);
      this.lruList.push(url);
    }
  }
  
  evict() {
    if (this.lruList.length === 0) return;
    
    const url = this.lruList.shift();
    const texture = this.cache.get(url);
    
    if (texture) {
      texture.dispose();
      this.cache.delete(url);
    }
  }
  
  preload(urls, options = {}) {
    return Promise.all(urls.map(url => this.load(url, options)));
  }
  
  clear() {
    this.cache.forEach(texture => texture.dispose());
    this.cache.clear();
    this.lruList = [];
  }
  
  getStats() {
    let totalSize = 0;
    this.cache.forEach(texture => {
      if (texture.image) {
        totalSize += texture.image.width * texture.image.height * 4;  // RGBA
      }
    });
    
    return {
      count: this.cache.size,
      maxSize: this.maxSize,
      totalMemory: (totalSize / 1024 / 1024).toFixed(2) + ' MB',
      loading: this.loading.size
    };
  }
}
 
// 使用
const textureManager = new TextureManager(50);
 
// 加载纹理
const texture = await textureManager.load('texture.jpg', {
  encoding: THREE.sRGBEncoding,
  anisotropy: 4
});
 
// 预加载多个纹理
await textureManager.preload(['tex1.jpg', 'tex2.jpg', 'tex3.jpg']);
 
// 查看统计
console.log(textureManager.getStats());

纹理内存计算

javascript
// 计算纹理内存占用
function calculateTextureMemory(texture) {
  if (!texture.image) return 0;
  
  const width = texture.image.width;
  const height = texture.image.height;
  
  // 根据 format 确定字节数
  let bytesPerPixel = 4;  // RGBA
  
  if (texture.format === THREE.RGBAFormat) bytesPerPixel = 4;
  else if (texture.format === THREE.RGBFormat) bytesPerPixel = 3;
  else if (texture.format === THREE.LuminanceAlphaFormat) bytesPerPixel = 2;
  else if (texture.format === THREE.LuminanceFormat) bytesPerPixel = 1;
  
  // 考虑 Mipmap
  let totalSize = width * height * bytesPerPixel;
  
  if (texture.generateMipmaps) {
    totalSize *= 1.33;  // Mipmap 增加 33%
  }
  
  return totalSize;
}
 
// 计算场景中所有纹理的内存
function calculateSceneTextureMemory(scene) {
  const textures = new Set();
  
  scene.traverse(obj => {
    if (obj.material) {
      const textureProps = [
        'map', 'normalMap', 'roughnessMap', 'metalnessMap',
        'aoMap', 'emissiveMap', 'alphaMap'
      ];
      
      const materials = Array.isArray(obj.material) ? obj.material : [obj.material];
      
      materials.forEach(mat => {
        textureProps.forEach(prop => {
          if (mat[prop]) {
            textures.add(mat[prop]);
          }
        });
      });
    }
  });
  
  let totalMemory = 0;
  const details = [];
  
  textures.forEach(texture => {
    const size = calculateTextureMemory(texture);
    totalMemory += size;
    
    details.push({
      uuid: texture.uuid,
      size: texture.image ? `${texture.image.width}x${texture.image.height}` : 'unknown',
      memory: (size / 1024 / 1024).toFixed(2) + ' MB'
    });
  });
  
  return {
    count: textures.size,
    totalMemory: (totalMemory / 1024 / 1024).toFixed(2) + ' MB',
    details
  };
}

几何体缓存

javascript
class GeometryCache {
  constructor() {
    this.cache = new Map();
    this.stats = {
      hits: 0,
      misses: 0
    };
  }
  
  get(key, factory) {
    if (this.cache.has(key)) {
      this.stats.hits++;
      return this.cache.get(key);
    }
    
    this.stats.misses++;
    const geometry = factory();
    this.cache.set(key, geometry);
    
    return geometry;
  }
  
  has(key) {
    return this.cache.has(key);
  }
  
  delete(key) {
    const geometry = this.cache.get(key);
    if (geometry) {
      geometry.dispose();
      this.cache.delete(key);
    }
  }
  
  clear() {
    this.cache.forEach(geometry => geometry.dispose());
    this.cache.clear();
  }
  
  getStats() {
    const total = this.stats.hits + this.stats.misses;
    const hitRate = total > 0 ? (this.stats.hits / total * 100).toFixed(1) : 0;
    
    return {
      size: this.cache.size,
      hits: this.stats.hits,
      misses: this.stats.misses,
      hitRate: hitRate + '%'
    };
  }
}
 
// 使用
const geometryCache = new GeometryCache();
 
const box = geometryCache.get('box-1x1x1', () => new THREE.BoxGeometry(1, 1, 1));
const sphere = geometryCache.get('sphere-32', () => new THREE.SphereGeometry(1, 32, 32));
 
console.log(geometryCache.getStats());

共享几何体管理

javascript
class SharedGeometryManager {
  constructor() {
    this.geometries = new Map();
    this.referenceCounts = new Map();
  }
  
  createBox(width, height, depth) {
    const key = `box-${width}-${height}-${depth}`;
    return this.getOrCreate(key, () => new THREE.BoxGeometry(width, height, depth));
  }
  
  createSphere(radius, widthSegments, heightSegments) {
    const key = `sphere-${radius}-${widthSegments}-${heightSegments}`;
    return this.getOrCreate(key, () => new THREE.SphereGeometry(radius, widthSegments, heightSegments));
  }
  
  createPlane(width, height) {
    const key = `plane-${width}-${height}`;
    return this.getOrCreate(key, () => new THREE.PlaneGeometry(width, height));
  }
  
  getOrCreate(key, factory) {
    if (this.geometries.has(key)) {
      const count = this.referenceCounts.get(key);
      this.referenceCounts.set(key, count + 1);
      return this.geometries.get(key);
    }
    
    const geometry = factory();
    this.geometries.set(key, geometry);
    this.referenceCounts.set(key, 1);
    
    return geometry;
  }
  
  release(geometry) {
    const key = this.findKey(geometry);
    if (!key) return;
    
    const count = this.referenceCounts.get(key);
    
    if (count <= 1) {
      geometry.dispose();
      this.geometries.delete(key);
      this.referenceCounts.delete(key);
    } else {
      this.referenceCounts.set(key, count - 1);
    }
  }
  
  findKey(geometry) {
    for (const [key, geo] of this.geometries) {
      if (geo === geometry) return key;
    }
    return null;
  }
  
  getStats() {
    const stats = {
      uniqueGeometries: this.geometries.size,
      totalReferences: Array.from(this.referenceCounts.values()).reduce((a, b) => a + b, 0)
    };
    
    return stats;
  }
}
 
// 使用
const sharedGeoManager = new SharedGeometryManager();
 
// 创建共享几何体
const box1 = sharedGeoManager.createBox(1, 1, 1);  // refCount: 1
const box2 = sharedGeoManager.createBox(1, 1, 1);  // refCount: 2 (同一几何体)
 
console.log(sharedGeoManager.getStats());
 
// 释放
sharedGeoManager.release(box1);  // refCount: 1
sharedGeoManager.release(box2);  // refCount: 0, 几何体被释放

最佳实践

1. 及时释放

不再使用的资源立即调用 dispose()

javascript
// 创建资源
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshStandardMaterial();
 
// 使用资源...
 
// 不再需要时立即释放
geometry.dispose();
material.dispose();

2. 避免重复创建

相同资源只创建一次并复用。

javascript
// 不推荐:每次都创建新材质
function createMesh(color) {
  return new THREE.Mesh(
    new THREE.BoxGeometry(),
    new THREE.MeshStandardMaterial({ color })
  );
}
 
// 推荐:复用材质
const material = new THREE.MeshStandardMaterial();
function createMesh() {
  return new THREE.Mesh(new THREE.BoxGeometry(), material);
}

3. 使用资源管理器

统一管理所有资源。

javascript
const resourceManager = new ResourceManager();
 
// 所有资源通过管理器创建
const texture = resourceManager.add('tex1', loader.load('texture.jpg'), 'texture');
 
// 统一释放
resourceManager.disposeAll();

4. 监控内存

定期检查内存使用情况。

javascript
const detector = new MemoryLeakDetector();
detector.start();
 
// 定期查看报告
setInterval(() => {
  console.log(detector.getReport());
}, 30000);

5. 清理事件监听

移除不再需要的事件监听器。

javascript
class EventManager {
  constructor() {
    this.listeners = [];
  }
  
  add(element, event, handler) {
    element.addEventListener(event, handler);
    this.listeners.push({ element, event, handler });
  }
  
  removeAll() {
    this.listeners.forEach(({ element, event, handler }) => {
      element.removeEventListener(event, handler);
    });
    this.listeners = [];
  }
}
 
// 使用
const eventManager = new EventManager();
eventManager.add(window, 'resize', onResize);
eventManager.add(document, 'click', onClick);
 
// 清理
eventManager.removeAll();

6. 资源生命周期管理

javascript
class ResourceLifecycleManager {
  constructor() {
    this.resources = new Map();
  }
  
  register(id, resource, options = {}) {
    this.resources.set(id, {
      resource,
      createdAt: Date.now(),
      lastAccessed: Date.now(),
      maxAge: options.maxAge || Infinity,
      autoDispose: options.autoDispose !== false
    });
  }
  
  access(id) {
    const item = this.resources.get(id);
    if (item) {
      item.lastAccessed = Date.now();
    }
    return item?.resource;
  }
  
  cleanup() {
    const now = Date.now();
    const toDelete = [];
    
    this.resources.forEach((item, id) => {
      const age = now - item.lastAccessed;
      if (age > item.maxAge) {
        toDelete.push(id);
      }
    });
    
    toDelete.forEach(id => {
      const item = this.resources.get(id);
      if (item.autoDispose && item.resource.dispose) {
        item.resource.dispose();
      }
      this.resources.delete(id);
    });
    
    return toDelete.length;
  }
}

API 参考

dispose() 方法

资源类型方法说明
BufferGeometrydispose()释放 GPU 缓冲区
Texturedispose()释放纹理数据
Materialdispose()释放材质程序
WebGLRenderTargetdispose()释放渲染目标
WebGLRendererdispose()释放所有资源

Performance.memory API

属性类型说明
usedJSHeapSizeNumber已使用的堆大小
totalJSHeapSizeNumber总堆大小
jsHeapSizeLimitNumber堆大小限制

WebGLRenderer.info.memory 属性

属性类型说明
geometriesNumber内存中的几何体数量
texturesNumber内存中的纹理数量

常见问题

Q1: 如何判断是否存在内存泄漏?

检测方法:

javascript
// 1. 使用 MemoryLeakDetector
const detector = new MemoryLeakDetector();
detector.start();
 
// 2. 观察 renderer.info.memory
function monitorMemory() {
  const prev = {
    geometries: renderer.info.memory.geometries,
    textures: renderer.info.memory.textures
  };
  
  return () => {
    const curr = {
      geometries: renderer.info.memory.geometries,
      textures: renderer.info.memory.textures
    };
    
    if (curr.geometries > prev.geometries || curr.textures > prev.textures) {
      console.warn('资源数量增长:', {
        geometries: `${prev.geometries} -> ${curr.geometries}`,
        textures: `${prev.textures} -> ${curr.textures}`
      });
    }
    
    return { prev, curr };
  };
}
 
// 3. 使用 Chrome DevTools
// - Memory 面板 -> Take heap snapshot
// - 对比多个快照
// - 查找 Detached nodes

Q2: dispose() 后资源未释放怎么办?

可能原因及解决:

javascript
// 1. 还有引用存在
let mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
 
// 释放时需要清除所有引用
scene.remove(mesh);
mesh.geometry.dispose();
mesh.material.dispose();
mesh = null;  // 清除引用
 
// 2. 纹理被多个材质共享
const texture = new THREE.Texture();
material1.map = texture;
material2.map = texture;
 
// 需要等所有材质释放后再释放纹理
material1.dispose();
material2.dispose();
texture.dispose();
 
// 3. 使用资源管理器统一管理
const manager = new ResourceManager();
manager.add('texture', texture, 'texture');
// 只通过管理器释放
manager.dispose('texture');

Q3: 如何处理大场景的资源管理?

分层管理策略:

javascript
class LargeSceneResourceManager {
  constructor() {
    this.regions = new Map();
    this.activeRegion = null;
  }
  
  registerRegion(id, loader) {
    this.regions.set(id, {
      loader,
      loaded: false,
      resources: []
    });
  }
  
  async loadRegion(id) {
    const region = this.regions.get(id);
    if (region.loaded) return;
    
    // 加载区域资源
    const resources = await region.loader();
    region.resources = resources;
    region.loaded = true;
    
    // 记录资源
    resources.forEach(r => resourceManager.add(r.id, r.resource, r.type));
  }
  
  async unloadRegion(id) {
    const region = this.regions.get(id);
    if (!region.loaded) return;
    
    // 释放资源
    region.resources.forEach(r => {
      resourceManager.dispose(r.id);
    });
    
    region.resources = [];
    region.loaded = false;
  }
  
  async switchRegion(id) {
    // 卸载旧区域
    if (this.activeRegion) {
      await this.unloadRegion(this.activeRegion);
    }
    
    // 加载新区域
    await this.loadRegion(id);
    this.activeRegion = id;
  }
}

Q4: 移动端内存优化建议?

移动端优化策略:

javascript
// 1. 限制纹理大小
function loadTextureForMobile(url) {
  const maxSize = 1024;
  
  return new Promise((resolve) => {
    const img = new Image();
    img.onload = () => {
      // 缩放大图片
      if (img.width > maxSize || img.height > maxSize) {
        const canvas = document.createElement('canvas');
        const scale = Math.min(maxSize / img.width, maxSize / img.height);
        canvas.width = img.width * scale;
        canvas.height = img.height * scale;
        
        const ctx = canvas.getContext('2d');
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
        
        const texture = new THREE.CanvasTexture(canvas);
        resolve(texture);
      } else {
        const texture = new THREE.Texture(img);
        resolve(texture);
      }
    };
    img.src = url;
  });
}
 
// 2. 及时清理缓存
class MobileResourceManager extends ResourceManager {
  constructor() {
    super();
    this.maxTextures = 20;
    this.maxGeometries = 50;
  }
  
  add(id, resource, type) {
    // 检查数量限制
    if (type === 'texture' && this.textures.size >= this.maxTextures) {
      this.evictOldest('texture');
    }
    
    if (type === 'geometry' && this.geometries.size >= this.maxGeometries) {
      this.evictOldest('geometry');
    }
    
    return super.add(id, resource, type);
  }
  
  evictOldest(type) {
    const map = type === 'texture' ? this.textures : this.geometries;
    
    // 找到最旧的资源并释放
    const firstKey = map.keys().next().value;
    if (firstKey) {
      this.dispose(firstKey);
    }
  }
}
 
// 3. 定期清理
setInterval(() => {
  if (performance.memory) {
    const used = performance.memory.usedJSHeapSize / 1024 / 1024;
    if (used > 50) {  // 超过 50MB
      console.warn('移动端内存过高,建议清理');
      // 触发清理
      resourceManager.disposeAll();
    }
  }
}, 30000);

Q5: 如何实现资源的延迟加载和卸载?

延迟加载系统:

javascript
class LazyResourceManager {
  constructor() {
    this.loadedResources = new Map();
    this.pendingLoads = new Map();
    this.unloadTimers = new Map();
    this.unloadDelay = 60000;  // 60秒后卸载
  }
  
  async load(key, loader) {
    // 已加载,更新最后访问时间
    if (this.loadedResources.has(key)) {
      this.updateAccessTime(key);
      return this.loadedResources.get(key).resource;
    }
    
    // 正在加载中
    if (this.pendingLoads.has(key)) {
      return this.pendingLoads.get(key);
    }
    
    // 开始加载
    const loadPromise = loader();
    this.pendingLoads.set(key, loadPromise);
    
    const resource = await loadPromise;
    
    this.loadedResources.set(key, {
      resource,
      loadedAt: Date.now(),
      lastAccessed: Date.now()
    });
    
    this.pendingLoads.delete(key);
    
    return resource;
  }
  
  updateAccessTime(key) {
    const item = this.loadedResources.get(key);
    if (item) {
      item.lastAccessed = Date.now();
      
      // 取消卸载计时器
      if (this.unloadTimers.has(key)) {
        clearTimeout(this.unloadTimers.get(key));
        this.unloadTimers.delete(key);
      }
    }
  }
  
  scheduleUnload(key) {
    // 设置卸载计时器
    const timer = setTimeout(() => {
      this.unload(key);
    }, this.unloadDelay);
    
    this.unloadTimers.set(key, timer);
  }
  
  unload(key) {
    const item = this.loadedResources.get(key);
    if (item && item.resource.dispose) {
      item.resource.dispose();
    }
    
    this.loadedResources.delete(key);
    this.unloadTimers.delete(key);
  }
  
  unloadAll() {
    this.loadedResources.forEach((item, key) => {
      if (item.resource.dispose) {
        item.resource.dispose();
      }
    });
    
    this.loadedResources.clear();
    this.unloadTimers.forEach(timer => clearTimeout(timer));
    this.unloadTimers.clear();
  }
}

相关链接