{T}

性能优化与调试

性能瓶颈分析

性能瓶颈类型

瓶颈类型症状原因
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 snapshot

WebGL 性能查询

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 调试信息:

  1. 安装 Chrome 扩展
  2. 打开开发者工具
  3. 查看 WebGL 调用序列
  4. 分析纹理和着色器

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);
  }
}

小结

核心优化策略

  1. 减少绘制调用:批处理、实例化渲染
  2. 优化着色器:减少分支、向量化计算
  3. 合理使用纹理:压缩、Mipmap、图集
  4. 状态管理:减少状态切换、缓存状态

调试技巧

  1. 错误检查:包装 WebGL 方法
  2. 性能分析:Chrome DevTools、Spector.js
  3. 内存监控:定期检查内存使用
  4. 帧率监控:实时 FPS 显示

下一步

继续学习实战案例,综合应用所学知识完成项目。