性能分析
性能分析是优化 Three.js 应用的第一步,通过系统化的监控和分析找出性能瓶颈,为后续优化提供数据支撑。
概述
Three.js 应用性能受多种因素影响,包括渲染效率、内存使用、CPU 计算等。性能分析的核心目标是:
- 识别瓶颈:找出影响性能的关键因素
- 量化评估:通过具体数据评估优化效果
- 持续监控:实时监控运行时性能状态
- 预防问题:在问题发生前发现潜在风险
性能分析流程
plaintext
┌─────────────┐
│ 建立基准 │ ──> 测试初始性能指标
└──────┬──────┘
│
▼
┌─────────────┐
│ 监控采集 │ ──> 使用工具收集运行数据
└──────┬──────┘
│
▼
┌─────────────┐
│ 分析定位 │ ──> 识别性能瓶颈位置
└──────┬──────┘
│
▼
┌─────────────┐
│ 优化改进 │ ──> 针对性优化处理
└──────┬──────┘
│
▼
┌─────────────┐
│ 验证效果 │ ──> 对比优化前后数据
└─────────────┘系统架构
性能分析系统组成
plaintext
┌────────────────────────────────────────────────────────┐
│ 性能分析系统架构 │
├────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 数据采集层 │ │ 分析处理层 │ │ 可视化层 │ │
│ ├──────────────┤ ├──────────────┤ ├──────────────┤ │
│ │ • Stats.js │ │ • FPS 计算 │ │ • 实时面板 │ │
│ │ • Performance│ │ • 渲染计时 │ │ • 控制台报告 │ │
│ │ API │ │ • 内存分析 │ │ • 图表展示 │ │
│ │ • WebGL Info │ │ • 泄漏检测 │ │ • 导出报告 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
└────────────────────────────────────────────────────────┘核心监控指标
| 指标类型 | 指标名称 | 说明 | 目标值 |
|---|---|---|---|
| 帧率 | FPS | 每秒渲染帧数 | ≥ 60 FPS |
| 帧率 | 帧时间 | 单帧渲染时间 | ≤ 16.67ms |
| 渲染 | Draw Calls | 每帧绘制调用次数 | 尽可能少 |
| 渲染 | 三角形数量 | 每帧渲染的三角形数 | 根据设备调整 |
| 内存 | JS 堆内存 | JavaScript 内存使用 | 无持续增长 |
| 内存 | GPU 内存 | 纹理、几何体占用 | < 显存限制 |
| 资源 | 几何体数量 | 场景中几何体数 | 合理范围 |
| 资源 | 纹理数量 | 场景中纹理数 | 合理范围 |
性能指标参考
FPS 性能标准
| 性能等级 | FPS 范围 | 用户体验 | 建议操作 |
|---|---|---|---|
| 优秀 | ≥ 60 | 流畅,无卡顿 | 保持现状 |
| 良好 | 45-59 | 基本流畅 | 可接受,持续监控 |
| 一般 | 30-44 | 有轻微卡顿 | 需要优化 |
| 较差 | 15-29 | 明显卡顿 | 必须优化 |
| 极差 | < 15 | 无法使用 | 紧急优化 |
设备性能分级
| 设备类型 | 三角形数 | Draw Calls | 纹理内存 | 建议 |
|---|---|---|---|---|
| 高端 PC | < 5M | < 2000 | < 512MB | 可使用完整特效 |
| 中端 PC | < 2M | < 1000 | < 256MB | 适度降级 |
| 低端 PC | < 500K | < 500 | < 128MB | 大幅降级 |
| 高端移动 | < 500K | < 300 | < 128MB | 简化效果 |
| 中端移动 | < 200K | < 150 | < 64MB | 基础效果 |
| 低端移动 | < 50K | < 50 | < 32MB | 最简配置 |
Stats.js 性能监控
基础使用
Stats.js 是最常用的性能监控工具,可以实时显示 FPS、帧时间和内存使用。
javascript
import Stats from 'three/addons/libs/stats.module.js';
// 创建性能监视器
const stats = new Stats();
stats.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom
document.body.appendChild(stats.dom);
// 在渲染循环中更新
function animate() {
stats.begin();
// 渲染代码
renderer.render(scene, camera);
stats.end();
requestAnimationFrame(animate);
}
animate();多面板配置
Stats.js 提供多种监控面板:
| 面板 ID | 类型 | 说明 | 适用场景 |
|---|---|---|---|
| 0 | FPS | 帧率监控 | 基础性能监控 |
| 1 | MS | 帧渲染时间 | 分析渲染耗时 |
| 2 | MB | 内存使用 | 内存泄漏检测 |
| 3+ | Custom | 自定义指标 | 特定功能监控 |
javascript
const stats = new Stats();
// FPS 面板
stats.showPanel(0);
// MS 面板(渲染时间)
stats.showPanel(1);
// MB 面板(内存使用)
stats.showPanel(2);
// 切换面板(点击)
stats.dom.style.position = 'absolute';
stats.dom.style.left = '0px';
stats.dom.style.top = '0px';
document.body.appendChild(stats.dom);自定义监控面板
javascript
// 创建自定义面板
const stats = new Stats();
stats.showPanel(3); // 自定义面板
// 自定义监控逻辑
const customPanel = stats.addPanel(
new Stats.Panel('Objects', '#ff8', '#221')
);
let objectCount = 0;
function animate() {
stats.begin();
// 更新自定义面板
customPanel.update(objectCount, 1000);
// 渲染逻辑...
objectCount = scene.children.length;
stats.end();
requestAnimationFrame(animate);
}浏览器开发工具
Performance 面板
使用浏览器 Performance API 进行精确的性能分析。
javascript
// 使用 performance API 标记时间点
performance.mark('render-start');
renderer.render(scene, camera);
performance.mark('render-end');
performance.measure('render', 'render-start', 'render-end');
// 获取测量结果
const measures = performance.getEntriesByName('render');
console.log('渲染时间:', measures[0].duration, 'ms');
// 清理
performance.clearMarks();
performance.clearMeasures();性能分析函数
javascript
// 高精度性能分析
function measurePerformance(label, fn) {
performance.mark(`${label}-start`);
const result = fn();
performance.mark(`${label}-end`);
performance.measure(label, `${label}-start`, `${label}-end`);
const measure = performance.getEntriesByName(label)[0];
console.log(`${label}: ${measure.duration.toFixed(2)}ms`);
performance.clearMarks();
performance.clearMeasures();
return result;
}
// 使用示例
measurePerformance('scene-update', () => {
scene.traverse(obj => {
// 更新逻辑
});
});Memory 面板
javascript
// 监控内存使用
if (performance.memory) {
console.log('已使用堆大小:', performance.memory.usedJSHeapSize / 1024 / 1024, 'MB');
console.log('总堆大小:', performance.memory.totalJSHeapSize / 1024 / 1024, 'MB');
console.log('堆限制:', performance.memory.jsHeapSizeLimit / 1024 / 1024, 'MB');
}
// 定期监控
setInterval(() => {
if (performance.memory) {
const used = performance.memory.usedJSHeapSize / 1024 / 1024;
console.log(`内存使用: ${used.toFixed(2)} MB`);
}
}, 1000);内存监控类
javascript
class MemoryMonitor {
constructor() {
this.samples = [];
this.maxSamples = 100;
this.warningThreshold = 100 * 1024 * 1024; // 100MB
}
sample() {
if (performance.memory) {
const memory = {
timestamp: Date.now(),
used: performance.memory.usedJSHeapSize,
total: performance.memory.totalJSHeapSize
};
this.samples.push(memory);
if (this.samples.length > this.maxSamples) {
this.samples.shift();
}
// 检查阈值
if (memory.used > this.warningThreshold) {
console.warn(`内存使用过高: ${(memory.used / 1024 / 1024).toFixed(2)} MB`);
}
return memory;
}
return null;
}
getStats() {
if (this.samples.length === 0) return null;
const used = this.samples.map(s => s.used);
return {
current: used[used.length - 1] / 1024 / 1024,
average: used.reduce((a, b) => a + b) / used.length / 1024 / 1024,
peak: Math.max(...used) / 1024 / 1024,
min: Math.min(...used) / 1024 / 1024
};
}
}
// 使用
const memoryMonitor = new MemoryMonitor();
setInterval(() => memoryMonitor.sample(), 1000);Three.js 内置分析
Renderer 信息
WebGLRenderer 提供了丰富的渲染统计信息。
javascript
// 渲染器信息
console.log('渲染器:', renderer.info.render);
console.log('几何体数量:', renderer.info.memory.geometries);
console.log('纹理数量:', renderer.info.memory.textures);
console.log('程序数量:', renderer.info.programs);
// 在动画循环中监控
function animate() {
requestAnimationFrame(animate);
console.log('Draw Calls:', renderer.info.render.calls);
console.log('三角形数量:', renderer.info.render.triangles);
console.log('点数量:', renderer.info.render.points);
renderer.render(scene, camera);
}renderer.info 属性详解
| 属性路径 | 类型 | 说明 |
|---|---|---|
info.render.calls | Number | 每帧 Draw Calls 数量 |
info.render.triangles | Number | 渲染的三角形数量 |
info.render.points | Number | 渲染的点数量 |
info.render.lines | Number | 渲染的线段数量 |
info.render.frame | Number | 帧计数 |
info.memory.geometries | Number | 几何体数量 |
info.memory.textures | Number | 纹理数量 |
info.programs | Array | Shader 程序列表 |
WebGL 信息
获取 WebGL 上下文的详细信息。
javascript
// WebGL 上下文信息
const gl = renderer.getContext();
console.log('WebGL 版本:', gl.getParameter(gl.VERSION));
console.log('GLSL 版本:', gl.getParameter(gl.SHADING_LANGUAGE_VERSION));
console.log('厂商:', gl.getParameter(gl.VENDOR));
console.log('渲染器:', gl.getParameter(gl.RENDERER));
// 纹理单元数量
console.log('纹理单元:', gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS));
// 顶点属性数量
console.log('顶点属性:', gl.getParameter(gl.MAX_VERTEX_ATTRIBS));
// 最大纹理尺寸
console.log('最大纹理尺寸:', gl.getParameter(gl.MAX_TEXTURE_SIZE));
// 各向异性过滤
const ext = gl.getExtension('EXT_texture_filter_anisotropic');
if (ext) {
console.log('最大各向异性:', gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT));
}WebGL 能力检测
javascript
function getWebGLCapabilities(renderer) {
const gl = renderer.getContext();
return {
version: gl.getParameter(gl.VERSION),
glslVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION),
vendor: gl.getParameter(gl.VENDOR),
renderer: gl.getParameter(gl.RENDERER),
maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
maxTextureUnits: gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS),
maxVertexAttribs: gl.getParameter(gl.MAX_VERTEX_ATTRIBS),
maxVaryingVectors: gl.getParameter(gl.MAX_VARYING_VECTORS),
maxVertexUniformVectors: gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS),
maxFragmentUniformVectors: gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS)
};
}
// 使用
const capabilities = getWebGLCapabilities(renderer);
console.table(capabilities);自定义性能分析器
帧率计算器
javascript
class FPSCounter {
constructor() {
this.frames = 0;
this.lastTime = performance.now();
this.fps = 0;
this.history = [];
this.maxHistory = 60;
}
update() {
this.frames++;
const currentTime = performance.now();
const delta = currentTime - this.lastTime;
if (delta >= 1000) {
this.fps = this.frames * 1000 / delta;
this.history.push(this.fps);
if (this.history.length > this.maxHistory) {
this.history.shift();
}
this.frames = 0;
this.lastTime = currentTime;
}
}
getAverageFPS() {
if (this.history.length === 0) return 0;
return this.history.reduce((a, b) => a + b) / this.history.length;
}
getMinFPS() {
if (this.history.length === 0) return 0;
return Math.min(...this.history);
}
getMaxFPS() {
if (this.history.length === 0) return 0;
return Math.max(...this.history);
}
getStats() {
return {
current: this.fps,
average: this.getAverageFPS(),
min: this.getMinFPS(),
max: this.getMaxFPS()
};
}
}
// 使用
const fpsCounter = new FPSCounter();
function animate() {
requestAnimationFrame(animate);
fpsCounter.update();
console.log(`FPS: ${fpsCounter.fps.toFixed(1)}`);
console.log(`平均 FPS: ${fpsCounter.getAverageFPS().toFixed(1)}`);
renderer.render(scene, camera);
}渲染时间分析器
javascript
class RenderProfiler {
constructor() {
this.timings = {};
this.enabled = true;
this.history = {};
}
start(label) {
if (!this.enabled) return;
this.timings[label] = performance.now();
}
end(label) {
if (!this.enabled || !this.timings[label]) return;
const duration = performance.now() - this.timings[label];
// 记录历史
if (!this.history[label]) {
this.history[label] = [];
}
this.history[label].push(duration);
// 限制历史长度
if (this.history[label].length > 60) {
this.history[label].shift();
}
console.log(`${label}: ${duration.toFixed(2)}ms`);
delete this.timings[label];
}
profile(label, fn) {
this.start(label);
const result = fn();
this.end(label);
return result;
}
getStats(label) {
const history = this.history[label];
if (!history || history.length === 0) return null;
return {
average: history.reduce((a, b) => a + b) / history.length,
min: Math.min(...history),
max: Math.max(...history),
samples: history.length
};
}
reset() {
this.timings = {};
this.history = {};
}
}
// 使用
const profiler = new RenderProfiler();
function animate() {
requestAnimationFrame(animate);
profiler.start('total');
profiler.start('update');
updateScene();
profiler.end('update');
profiler.start('render');
renderer.render(scene, camera);
profiler.end('render');
profiler.end('total');
// 获取统计信息
console.log(profiler.getStats('render'));
}场景分析器
javascript
class SceneAnalyzer {
constructor(scene) {
this.scene = scene;
}
analyze() {
const result = {
meshes: 0,
triangles: 0,
vertices: 0,
materials: new Set(),
geometries: new Set(),
textures: new Set(),
lights: 0,
cameras: 0
};
this.scene.traverse((object) => {
if (object.isMesh) {
result.meshes++;
if (object.geometry) {
result.geometries.add(object.geometry);
if (object.geometry.index) {
result.triangles += object.geometry.index.count / 3;
} else {
result.triangles += object.geometry.attributes.position.count / 3;
}
result.vertices += object.geometry.attributes.position.count;
}
if (object.material) {
if (Array.isArray(object.material)) {
object.material.forEach(m => result.materials.add(m));
} else {
result.materials.add(object.material);
}
}
}
if (object.isLight) result.lights++;
if (object.isCamera) result.cameras++;
});
// 分析纹理
result.materials.forEach(material => {
const textureProps = ['map', 'normalMap', 'roughnessMap', 'metalnessMap', 'aoMap'];
textureProps.forEach(prop => {
if (material[prop]) {
result.textures.add(material[prop]);
}
});
});
return {
meshes: result.meshes,
triangles: Math.round(result.triangles),
vertices: result.vertices,
uniqueMaterials: result.materials.size,
uniqueGeometries: result.geometries.size,
uniqueTextures: result.textures.size,
lights: result.lights,
cameras: result.cameras
};
}
}
// 使用
const analyzer = new SceneAnalyzer(scene);
const analysis = analyzer.analyze();
console.log('场景分析:', analysis);性能分析最佳实践
分段分析策略
javascript
// 分段性能分析
class PerformanceAnalyzer {
constructor() {
this.sections = new Map();
}
startSection(name) {
this.sections.set(name, {
start: performance.now(),
end: null,
duration: null
});
}
endSection(name) {
const section = this.sections.get(name);
if (section) {
section.end = performance.now();
section.duration = section.end - section.start;
}
}
getReport() {
const report = {};
this.sections.forEach((value, key) => {
report[key] = value.duration ? value.duration.toFixed(2) + 'ms' : 'incomplete';
});
return report;
}
}
// 使用
const analyzer = new PerformanceAnalyzer();
function animate() {
analyzer.startSection('frame');
analyzer.startSection('physics');
updatePhysics();
analyzer.endSection('physics');
analyzer.startSection('animation');
updateAnimations();
analyzer.endSection('animation');
analyzer.startSection('render');
renderer.render(scene, camera);
analyzer.endSection('render');
analyzer.endSection('frame');
console.log(analyzer.getReport());
}性能报告生成
javascript
function generatePerformanceReport() {
const report = {
timestamp: new Date().toISOString(),
renderer: {
info: {
render: renderer.info.render,
memory: renderer.info.memory
},
capabilities: {
maxTextures: renderer.capabilities.maxTextures,
maxVertexTextures: renderer.capabilities.maxVertexTextures,
maxTextureSize: renderer.capabilities.maxTextureSize,
precision: renderer.capabilities.precision
}
},
memory: performance.memory ? {
used: (performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2) + ' MB',
total: (performance.memory.totalJSHeapSize / 1024 / 1024).toFixed(2) + ' MB',
limit: (performance.memory.jsHeapSizeLimit / 1024 / 1024).toFixed(2) + ' MB'
} : 'Not available',
performance: {
fps: fpsCounter.getStats(),
frameTime: profiler.getStats('frame')
}
};
return report;
}
// 生成报告
const report = generatePerformanceReport();
console.table(report.renderer.info.render);
console.table(report.memory);API 参考
Stats.js API
| 方法 | 参数 | 说明 |
|---|---|---|
showPanel(id) | id: Number | 显示指定面板(0: FPS, 1: MS, 2: MB) |
begin() | - | 开始计时 |
end() | - | 结束计时 |
update() | - | 更新显示(替代 begin/end) |
addPanel(panel) | panel: Panel | 添加自定义面板 |
Performance API
| 方法 | 参数 | 说明 |
|---|---|---|
performance.mark(name) | name: String | 创建命名时间戳 |
performance.measure(name, start, end) | name, start, end | 测量两个标记间的时间 |
performance.getEntriesByName(name) | name: String | 获取指定名称的测量结果 |
performance.clearMarks() | - | 清除所有标记 |
performance.clearMeasures() | - | 清除所有测量 |
WebGLRenderer.info 属性
| 属性 | 类型 | 说明 |
|---|---|---|
info.render.calls | Number | Draw Calls 数量 |
info.render.triangles | Number | 三角形数量 |
info.render.points | Number | 点数量 |
info.render.lines | Number | 线段数量 |
info.memory.geometries | Number | 几何体数量 |
info.memory.textures | Number | 纹理数量 |
常见问题
Q1: FPS 为什么不稳定?
原因分析:
- 垃圾回收(GC)导致的卡顿
- 复杂场景导致的渲染时间波动
- JavaScript 主线程阻塞
解决方案:
javascript
// 1. 避免在渲染循环中创建对象
// 不推荐
function animate() {
const position = new THREE.Vector3(); // 每帧创建新对象
// ...
}
// 推荐
const position = new THREE.Vector3(); // 复用对象
function animate() {
position.set(0, 0, 0);
// ...
}
// 2. 使用对象池
class ObjectPool {
constructor(factory, size = 10) {
this.pool = Array(size).fill(null).map(() => factory());
this.index = 0;
}
get() {
const obj = this.pool[this.index % this.pool.length];
this.index++;
return obj;
}
}Q2: Draw Calls 过高如何优化?
优化策略:
| 场景 | 优化方法 | 效果 |
|---|---|---|
| 多个相同几何体 | InstancedMesh | 大幅减少 |
| 多个不同几何体 | 几何体合并 | 中等减少 |
| 多个不同材质 | 材质共享 | 小幅减少 |
| 静态场景 | 预合并 | 大幅减少 |
javascript
// 使用 InstancedMesh 优化相同几何体
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial();
const count = 1000;
const mesh = new THREE.InstancedMesh(geometry, material, count);
// 设置实例矩阵
const matrix = new THREE.Matrix4();
for (let i = 0; i < count; i++) {
matrix.setPosition(Math.random() * 10, Math.random() * 10, Math.random() * 10);
mesh.setMatrixAt(i, matrix);
}
scene.add(mesh);
// Draw Calls: 1(而不是 1000)Q3: 如何检测内存泄漏?
检测方法:
javascript
// 1. 使用 Memory 监控
class MemoryLeakDetector {
constructor() {
this.samples = [];
}
sample() {
if (performance.memory) {
this.samples.push({
time: Date.now(),
used: performance.memory.usedJSHeapSize
});
}
}
detectLeak() {
if (this.samples.length < 20) return false;
const recent = this.samples.slice(-10);
const older = this.samples.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;
// 如果内存持续增长超过 20%
return recentAvg > olderAvg * 1.2;
}
}
// 2. 使用 Chrome DevTools Memory 面板
// - 录制堆快照
// - 对比多个快照
// - 查找 Detached DOM 节点Q4: 移动端性能优化建议?
优化策略:
javascript
// 1. 检测设备性能
function getDevicePerformance() {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) return 'low';
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
const renderer = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : '';
// 检测是否为低端设备
if (renderer.includes('Intel')) return 'medium';
if (renderer.includes('NVIDIA') || renderer.includes('AMD')) return 'high';
return 'medium';
}
// 2. 根据设备调整参数
const devicePerformance = getDevicePerformance();
const config = {
high: {
pixelRatio: Math.min(window.devicePixelRatio, 2),
shadows: true,
antialias: true,
maxTriangles: 2000000
},
medium: {
pixelRatio: Math.min(window.devicePixelRatio, 1.5),
shadows: true,
antialias: false,
maxTriangles: 500000
},
low: {
pixelRatio: 1,
shadows: false,
antialias: false,
maxTriangles: 100000
}
}[devicePerformance];
// 应用配置
renderer.setPixelRatio(config.pixelRatio);
renderer.shadowMap.enabled = config.shadows;Q5: 如何平衡画质和性能?
动态降级策略:
javascript
class PerformanceManager {
constructor() {
this.targetFPS = 60;
this.currentQuality = 'high';
this.qualityLevels = ['low', 'medium', 'high'];
this.fpsHistory = [];
}
update(fps) {
this.fpsHistory.push(fps);
if (this.fpsHistory.length > 60) {
this.fpsHistory.shift();
}
const avgFPS = this.fpsHistory.reduce((a, b) => a + b) / this.fpsHistory.length;
// 自动降级
if (avgFPS < this.targetFPS * 0.8) {
this.degradeQuality();
}
// 自动升级
else if (avgFPS > this.targetFPS * 0.95) {
this.upgradeQuality();
}
}
degradeQuality() {
const currentIndex = this.qualityLevels.indexOf(this.currentQuality);
if (currentIndex > 0) {
this.currentQuality = this.qualityLevels[currentIndex - 1];
this.applyQuality(this.currentQuality);
console.log('质量降级:', this.currentQuality);
}
}
upgradeQuality() {
const currentIndex = this.qualityLevels.indexOf(this.currentQuality);
if (currentIndex < this.qualityLevels.length - 1) {
this.currentQuality = this.qualityLevels[currentIndex + 1];
this.applyQuality(this.currentQuality);
console.log('质量升级:', this.currentQuality);
}
}
applyQuality(level) {
switch (level) {
case 'low':
renderer.setPixelRatio(1);
renderer.shadowMap.enabled = false;
break;
case 'medium':
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.5));
renderer.shadowMap.enabled = true;
break;
case 'high':
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
break;
}
}
}