内存管理
合理的内存管理对于 Three.js 应用的稳定运行至关重要。避免内存泄漏可以确保应用长时间运行而不崩溃,同时优化内存使用可以提升应用性能和用户体验。
概述
Three.js 应用涉及大量 GPU 资源(几何体、纹理、材质等),这些资源需要手动释放,否则会导致内存泄漏。内存管理的核心目标是:
- 防止泄漏:确保不再使用的资源被正确释放
- 优化使用:合理复用资源,避免重复创建
- 监控预警:实时监控内存状态,及时发现问题
- 生命周期管理:建立完善的资源生命周期管理体系
内存管理流程
plaintext
┌─────────────┐
│ 资源创建 │ ──> 记录资源引用
└──────┬──────┘
│
▼
┌─────────────┐
│ 资源使用 │ ──> 正常使用阶段
└──────┬──────┘
│
▼
┌─────────────┐
│ 资源标记 │ ──> 标记为不再使用
└──────┬──────┘
│
▼
┌─────────────┐
│ 资源释放 │ ──> 调用 dispose() 方法
└──────┬──────┘
│
▼
┌─────────────┐
│ 引用清理 │ ──> 清除所有引用
└─────────────┘系统架构
内存管理系统架构
plaintext
┌──────────────────────────────────────────────────────────┐
│ 内存管理系统架构 │
├──────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 资源层 │ │ 管理层 │ │ 监控层 │ │
│ ├──────────────┤ ├──────────────┤ ├──────────────┤ │
│ │ • 几何体 │ │ • 资源注册 │ │ • 使用统计 │ │
│ │ • 纹理 │ │ • 引用计数 │ │ • 泄漏检测 │ │
│ │ • 材质 │ │ • 自动释放 │ │ • 报警系统 │ │
│ │ • 渲染目标 │ │ • 缓存管理 │ │ • 日志记录 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ 生命周期管理器 │ │
│ ├────────────────────────────────────────────────┤ │
│ │ 创建 │ 使用 │ 暂停 │ 恢复 │ 释放 │ │
│ └────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────┘资源类型与生命周期
| 资源类型 | GPU 内存 | 释放方法 | 生命周期 | 管理难度 |
|---|---|---|---|---|
| BufferGeometry | 是 | dispose() | 场景级别 | 中 |
| Texture | 是 | dispose() | 材质级别 | 高 |
| Material | 是 | dispose() | 对象级别 | 中 |
| WebGLRenderTarget | 是 | dispose() | 功能级别 | 低 |
| ShaderMaterial | 是 | dispose() | 材质级别 | 中 |
内存指标参考
内存使用标准
| 平台 | 推荐内存 | 警告阈值 | 危险阈值 | 建议操作 |
|---|---|---|---|---|
| 桌面浏览器 | < 500MB | 500MB-1GB | > 1GB | 合理范围 |
| 移动浏览器 | < 100MB | 100MB-200MB | > 200MB | 需要优化 |
| 低端移动 | < 50MB | 50MB-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() 方法
| 资源类型 | 方法 | 说明 |
|---|---|---|
| BufferGeometry | dispose() | 释放 GPU 缓冲区 |
| Texture | dispose() | 释放纹理数据 |
| Material | dispose() | 释放材质程序 |
| WebGLRenderTarget | dispose() | 释放渲染目标 |
| WebGLRenderer | dispose() | 释放所有资源 |
Performance.memory API
| 属性 | 类型 | 说明 |
|---|---|---|
usedJSHeapSize | Number | 已使用的堆大小 |
totalJSHeapSize | Number | 总堆大小 |
jsHeapSizeLimit | Number | 堆大小限制 |
WebGLRenderer.info.memory 属性
| 属性 | 类型 | 说明 |
|---|---|---|
geometries | Number | 内存中的几何体数量 |
textures | Number | 内存中的纹理数量 |
常见问题
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 nodesQ2: 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();
}
}