性能优化与调试
性能瓶颈分析
性能瓶颈类型
| 瓶颈类型 | 症状 | 原因 |
|---|---|---|
| CPU 瓶颈 | 帧率低,CPU 占用高 | JavaScript 计算过多、API 调用频繁 |
| GPU 瓶颈 | 帧率低,GPU 占用高 | 复杂着色器、过度绘制 |
| 带宽瓶颈 | 纹理加载慢 | 纹理过大、频繁传输数据 |
| 内存瓶颈 | 页面崩溃 | 内存泄漏、资源未释放 |
性能分析工具
Chrome DevTools
javascript
// 1. Performance Monitor(性能监视器)
// 打开 DevTools → More tools → Performance Monitor
// 2. Performance 面板
// DevTools → Performance → 记录
// 3. Memory 面板
// DevTools → Memory → Take heap snapshotWebGL 性能查询
javascript
// 查询渲染统计信息(WebGL 2.0)
const ext = gl.getExtension('EXT_disjoint_timer_query_webgl2');
const query = gl.createQuery();
gl.beginQuery(ext.TIME_ELAPSED_EXT, query);
// 渲染操作
gl.drawElements(gl.TRIANGLES, indexCount, gl.UNSIGNED_SHORT, 0);
gl.endQuery(ext.TIME_ELAPSED_EXT);
// 读取结果
const available = gl.getQueryObject(query, gl.QUERY_RESULT_AVAILABLE);
if (available) {
const timeElapsed = gl.getQueryObject(query, gl.QUERY_RESULT);
console.log(`渲染耗时:${timeElapsed / 1000000} ms`);
}减少绘制调用
批处理
合并多个物体为一次绘制调用:
javascript
// ❌ 不好的做法:多次绘制
for (let i = 0; i < 100; i++) {
gl.uniformMatrix4fv(u_ModelMatrix, false, matrices[i]);
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
// ✅ 好的做法:实例化渲染
const modelMatrices = new Float32Array(100 * 16);
// ... 填充矩阵数据 ...
const matrixBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, matrixBuffer);
gl.bufferData(gl.ARRAY_BUFFER, modelMatrices, gl.STATIC_DRAW);
// 配置实例属性
for (let i = 0; i < 4; i++) {
const loc = gl.getAttribLocation(program, `a_ModelMatrix[${i}]`);
gl.enableVertexAttribArray(loc);
gl.vertexAttribPointer(loc, 4, gl.FLOAT, false, 64, i * 16);
gl.vertexAttribDivisor(loc, 1);
}
// 一次绘制所有实例
gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, 100);视锥剔除
只渲染相机可见的物体:
javascript
function isInFrustum(boundingBox, viewProjectionMatrix) {
// 获取包围盒的 8 个顶点
const corners = getBoundingBoxCorners(boundingBox);
// 变换到裁剪空间
for (const corner of corners) {
const clipSpace = multiplyMatrixVector(viewProjectionMatrix, corner);
// 检查是否在视锥内
if (clipSpace[0] >= -clipSpace[3] && clipSpace[0] <= clipSpace[3] &&
clipSpace[1] >= -clipSpace[3] && clipSpace[1] <= clipSpace[3] &&
clipSpace[2] >= -clipSpace[3] && clipSpace[2] <= clipSpace[3]) {
return true;
}
}
return false;
}
// 使用
for (const object of objects) {
if (isInFrustum(object.boundingBox, viewProjectionMatrix)) {
renderObject(object);
}
}遮挡剔除
使用查询对象检测物体是否被遮挡:
javascript
const ext = gl.getExtension('WEBGL_occlusion_query');
const query = ext.createQueryEXT();
ext.beginQueryEXT(ext.ANY_SAMPLES_PASSED, query);
// 渲染包围盒
renderBoundingBox(object);
ext.endQueryEXT(ext.ANY_SAMPLES_PASSED);
// 检查结果
const result = ext.getQueryObjectEXT(query, ext.QUERY_RESULT_EXT);
if (result > 0) {
// 物体可见,渲染完整模型
renderObject(object);
}着色器优化
减少分支
glsl
// ❌ 分支语句
if (lightCount > 0) {
for (int i = 0; i < lightCount; i++) {
// 光照计算
}
}
// ✅ 使用固定循环和条件判断
for (int i = 0; i < MAX_LIGHTS; i++) {
if (i < lightCount) {
// 光照计算
}
}向量化计算
glsl
// ❌ 标量计算
float r = color.r * 2.0;
float g = color.g * 2.0;
float b = color.b * 2.0;
// ✅ 向量计算
vec3 result = color.rgb * 2.0;避免重复计算
glsl
// ❌ 重复计算
float result = sin(angle) * sin(angle) + cos(angle) * cos(angle);
// ✅ 复用计算结果
float s = sin(angle);
float c = cos(angle);
float result = s * s + c * c;使用内置函数
glsl
// ❌ 手动实现
float length(vec3 v) {
return sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
}
// ✅ 使用内置函数
float len = length(v);精度优化
glsl
// 片段着色器:根据需求选择精度
precision highp float; // 高精度(位置计算)
precision mediump float; // 中精度(纹理坐标、颜色)
precision lowp float; // 低精度(简单颜色)纹理优化
纹理压缩
javascript
// 使用压缩纹理格式
const ext = gl.getExtension('WEBGL_compressed_texture_s3tc');
// 加载压缩纹理
gl.compressedTexImage2D(
gl.TEXTURE_2D,
0,
ext.COMPRESSED_RGB_S3TC_DXT1_EXT,
width,
height,
0,
data
);Mipmap
javascript
// 生成 Mipmap
gl.generateMipmap(gl.TEXTURE_2D);
// 设置 Mipmap 过滤
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);纹理图集
javascript
// 将多个小纹理打包成一个大纹理
const atlasSize = 1024;
const atlas = new Uint8Array(atlasSize * atlasSize * 4);
// 在着色器中使用纹理坐标偏移
// gl_FragColor = texture2D(u_Atlas, v_TexCoord * subTextureScale + subTextureOffset);纹理缓存
javascript
// 避免重复加载纹理
const textureCache = new Map();
function getTexture(url) {
if (textureCache.has(url)) {
return textureCache.get(url);
}
const texture = createTexture(url);
textureCache.set(url, texture);
return texture;
}缓冲区优化
数据布局优化
javascript
// ❌ 分离缓冲区
const positionBuffer = gl.createBuffer();
const colorBuffer = gl.createBuffer();
const normalBuffer = gl.createBuffer();
// ✅ 交错缓冲区
const vertices = new Float32Array([
// position (3) + color (3) + normal (3)
x, y, z, r, g, b, nx, ny, nz,
// ...
]);动态数据更新
javascript
// 使用 gl.bufferSubData 更新部分数据
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferSubData(gl.ARRAY_BUFFER, offset, newData);
// 使用 gl.mapBufferRange(WebGL 2.0)
const ptr = gl.mapBufferRange(gl.ARRAY_BUFFER, 0, size, gl.MAP_WRITE_BIT | gl.MAP_INVALIDATE_BUFFER_BIT);
// 写入数据
gl.unmapBuffer(gl.ARRAY_BUFFER);状态管理
减少 WebGL 状态切换
javascript
// ❌ 频繁切换状态
for (const object of objects) {
gl.useProgram(object.program);
gl.bindTexture(gl.TEXTURE_2D, object.texture);
renderObject(object);
}
// ✅ 按状态分组
const groups = groupByState(objects);
for (const group of groups) {
gl.useProgram(group.program);
gl.bindTexture(gl.TEXTURE_2D, group.texture);
for (const object of group.objects) {
renderObject(object);
}
}状态缓存
javascript
class WebGLState {
constructor(gl) {
this.gl = gl;
this.currentProgram = null;
this.boundTextures = new Array(32).fill(null);
}
useProgram(program) {
if (this.currentProgram !== program) {
this.gl.useProgram(program);
this.currentProgram = program;
}
}
bindTexture(unit, texture) {
if (this.boundTextures[unit] !== texture) {
this.gl.activeTexture(this.gl.TEXTURE0 + unit);
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
this.boundTextures[unit] = texture;
}
}
}调试工具
WebGL 错误检查
javascript
// 包装 WebGL 方法,自动检查错误
function wrapGLMethod(gl, methodName) {
const original = gl[methodName];
gl[methodName] = function(...args) {
const result = original.apply(this, args);
const error = this.getError();
if (error !== this.NO_ERROR) {
console.error(`WebGL 错误 in ${methodName}:`, getErrorString(this, error));
console.trace();
}
return result;
};
}
// 包装所有方法
for (const name in gl) {
if (typeof gl[name] === 'function') {
wrapGLMethod(gl, name);
}
}
function getErrorString(gl, error) {
const errors = {
[gl.INVALID_ENUM]: 'INVALID_ENUM',
[gl.INVALID_VALUE]: 'INVALID_VALUE',
[gl.INVALID_OPERATION]: 'INVALID_OPERATION',
[gl.INVALID_FRAMEBUFFER_OPERATION]: 'INVALID_FRAMEBUFFER_OPERATION',
[gl.OUT_OF_MEMORY]: 'OUT_OF_MEMORY',
[gl.CONTEXT_LOST_WEBGL]: 'CONTEXT_LOST_WEBGL'
};
return errors[error] || 'UNKNOWN_ERROR';
}WebGL Inspector
浏览器扩展,提供详细的 WebGL 调试信息:
- 安装 Chrome 扩展
- 打开开发者工具
- 查看 WebGL 调用序列
- 分析纹理和着色器
Spector.js
捕获和分析每一帧的 WebGL 调用:
javascript
const spector = new SPECTOR.Spector();
spector.spyCanvas(canvas);
// 捕获一帧
spector.captureFrame(1);常见性能问题
1. 过度绘制
javascript
// 开启深度测试
gl.enable(gl.DEPTH_TEST);
// 开启背面剔除
gl.enable(gl.CULL_FACE);
// 从前到后排序(提升性能)
objects.sort((a, b) => a.distance - b.distance);2. 内存泄漏
javascript
// 及时删除不再使用的资源
function cleanup() {
// 删除缓冲区
gl.deleteBuffer(vertexBuffer);
// 删除纹理
gl.deleteTexture(texture);
// 删除着色器程序
gl.deleteProgram(program);
// 删除帧缓冲
gl.deleteFramebuffer(framebuffer);
}3. Canvas 尺寸问题
javascript
// ❌ 使用 CSS 设置尺寸
canvas.style.width = '100%';
canvas.style.height = '100%';
// ✅ 设置实际像素尺寸
canvas.width = window.innerWidth * window.devicePixelRatio;
canvas.height = window.innerHeight * window.devicePixelRatio;
// 更新视口
gl.viewport(0, 0, canvas.width, canvas.height);性能优化清单
CPU 优化
- 减少 JavaScript 计算
- 使用 Web Workers
- 批量处理数据
- 避免频繁 DOM 操作
- 使用对象池
GPU 优化
- 减少绘制调用次数
- 使用实例化渲染
- 优化着色器代码
- 使用 VAO(WebGL 2.0)
- 减少状态切换
内存优化
- 及时释放资源
- 压缩纹理
- 使用纹理图集
- 限制纹理尺寸
- 避免内存泄漏
渲染优化
- 视锥剔除
- 遮挡剔除
- LOD(细节层次)
- 级联阴影
- 后处理优化
性能监控
实时帧率监控
javascript
class FPSMonitor {
constructor() {
this.frames = 0;
this.lastTime = performance.now();
this.fps = 0;
}
update() {
this.frames++;
const currentTime = performance.now();
const elapsed = currentTime - this.lastTime;
if (elapsed >= 1000) {
this.fps = Math.round((this.frames * 1000) / elapsed);
this.frames = 0;
this.lastTime = currentTime;
console.log(`FPS: ${this.fps}`);
}
}
}
// 使用
const fpsMonitor = new FPSMonitor();
function render() {
fpsMonitor.update();
// 渲染场景
requestAnimationFrame(render);
}性能统计面板
javascript
class PerformancePanel {
constructor() {
this.stats = {
drawCalls: 0,
triangles: 0,
textures: 0,
programs: 0
};
}
reset() {
this.stats.drawCalls = 0;
this.stats.triangles = 0;
}
addDrawCall(triangleCount) {
this.stats.drawCalls++;
this.stats.triangles += triangleCount;
}
log() {
console.log('性能统计:', this.stats);
}
}小结
核心优化策略
- 减少绘制调用:批处理、实例化渲染
- 优化着色器:减少分支、向量化计算
- 合理使用纹理:压缩、Mipmap、图集
- 状态管理:减少状态切换、缓存状态
调试技巧
- 错误检查:包装 WebGL 方法
- 性能分析:Chrome DevTools、Spector.js
- 内存监控:定期检查内存使用
- 帧率监控:实时 FPS 显示
下一步
继续学习实战案例,综合应用所学知识完成项目。