| 瓶颈类型 | 症状 | 原因 |
|---|---|---|
| CPU 瓶颈 | 帧率低,CPU 占用高 | JavaScript 计算过多、API 调用频繁 |
| GPU 瓶颈 | 帧率低,GPU 占用高 | 复杂着色器、过度绘制 |
| 带宽瓶颈 | 纹理加载慢 | 纹理过大、频繁传输数据 |
| 内存瓶颈 | 页面崩溃 | 内存泄漏、资源未释放 |
// 1. Performance Monitor(性能监视器)
// 打开 DevTools → More tools → Performance Monitor
// 2. Performance 面板
// DevTools → Performance → 记录
// 3. Memory 面板
// DevTools → Memory → Take heap snapshot// 查询渲染统计信息(WebGL 2.0)
// 注意:出于安全考虑,主流浏览器已陆续移除该扩展,使用前务必判空
const ext = gl.getExtension('EXT_disjoint_timer_query_webgl2');
if (!ext) {
throw new Error('当前浏览器不支持 GPU 计时查询');
}
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`);
}合并多个物体为一次绘制调用:
// ❌ 不好的做法:多次绘制
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);只渲染相机可见的物体:
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);
}
}使用查询对象检测物体是否被遮挡:
// WebGL 1.0 遮挡查询扩展(支持范围有限,使用前需判空)
const ext = gl.getExtension('EXT_occlusion_query_boolean');
const query = ext.createQueryEXT();
ext.beginQueryEXT(ext.ANY_SAMPLES_PASSED_EXT, query);
// 渲染包围盒
renderBoundingBox(object);
ext.endQueryEXT(ext.ANY_SAMPLES_PASSED_EXT);
// 检查结果
const result = ext.getQueryObjectEXT(query, ext.QUERY_RESULT_EXT);
if (result > 0) {
// 物体可见,渲染完整模型
renderObject(object);
}// ❌ 分支语句
if (lightCount > 0) {
for (int i = 0; i < lightCount; i++) {
// 光照计算
}
}
// ✅ 使用固定循环和条件判断
for (int i = 0; i < MAX_LIGHTS; i++) {
if (i < lightCount) {
// 光照计算
}
}// ❌ 标量计算
float r = color.r * 2.0;
float g = color.g * 2.0;
float b = color.b * 2.0;
// ✅ 向量计算
vec3 result = color.rgb * 2.0;// ❌ 重复计算
float result = sin(angle) * sin(angle) + cos(angle) * cos(angle);
// ✅ 复用计算结果
float s = sin(angle);
float c = cos(angle);
float result = s * s + c * c;// ❌ 手动实现(与内置函数同签名的重复定义本身就是编译错误)
float myLength(vec3 v) {
return sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
}
// ✅ 使用内置函数
float len = length(v);// 片段着色器:根据需求选择精度
precision highp float; // 高精度(位置计算)
precision mediump float; // 中精度(纹理坐标、颜色)
precision lowp float; // 低精度(简单颜色)// 使用压缩纹理格式
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
gl.generateMipmap(gl.TEXTURE_2D);
// 设置 Mipmap 过滤
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);// 将多个小纹理打包成一个大纹理
const atlasSize = 1024;
const atlas = new Uint8Array(atlasSize * atlasSize * 4);
// 在着色器中使用纹理坐标偏移
// gl_FragColor = texture2D(u_Atlas, v_TexCoord * subTextureScale + subTextureOffset);// 避免重复加载纹理
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;
}// ❌ 分离缓冲区
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,
// ...
]);// 使用 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);// ❌ 频繁切换状态
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);
}
}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 方法,自动检查错误
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 调试信息:
捕获和分析每一帧的 WebGL 调用:
const spector = new SPECTOR.Spector();
spector.spyCanvas(canvas);
// 捕获一帧
spector.captureFrame(1);// 开启深度测试
gl.enable(gl.DEPTH_TEST);
// 开启背面剔除
gl.enable(gl.CULL_FACE);
// 从前到后排序(提升性能)
objects.sort((a, b) => a.distance - b.distance);// 及时删除不再使用的资源
function cleanup() {
// 删除缓冲区
gl.deleteBuffer(vertexBuffer);
// 删除纹理
gl.deleteTexture(texture);
// 删除着色器程序
gl.deleteProgram(program);
// 删除帧缓冲
gl.deleteFramebuffer(framebuffer);
}// ❌ 使用 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);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);
}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);
}
}继续学习实战案例,综合应用所学知识完成项目。