Skip to content

第11章:性能优化与调试 ​

11.1 章节概述 ​

WebGL 应用的性能直接影响用户体验。本章将深入讲解如何优化 WebGL 应用的性能,以及如何调试常见问题。

本章将深入讲解:

  • 性能瓶颈分析:CPU vs GPU
  • Draw Call 优化:批处理、实例化
  • 着色器优化:避免分支、精度选择
  • 内存管理:纹理、缓冲区
  • 调试工具:Spector.js、Chrome DevTools

11.2 性能分析基础 ​

11.2.1 渲染管线瓶颈 ​

性能瓶颈识别

WebGL 渲染涉及 CPU 和 GPU 两端

CPU 端瓶颈:
──────────────────────────────────
- 过多的 Draw Call
- JavaScript 计算量大
- 频繁的状态切换
- 数据准备和上传

表现:GPU 空闲等待,帧率受 CPU 限制


GPU 端瓶颈:
──────────────────────────────────
- 顶点处理:复杂的顶点着色器、大量顶点
- 光栅化:大量小三角形
- 片段处理:复杂着色器、高分辨率、过度绘制
- 内存带宽:大纹理、频繁纹理切换

表现:CPU 等待 GPU 完成


判断方法:
──────────────────────────────────
1. 减少渲染分辨率
   - 帧率显著提升 → GPU 片段处理瓶颈
   - 帧率变化不大 → 可能是 CPU 或顶点瓶颈

2. 减少 Draw Call
   - 帧率提升 → CPU 瓶颈
   
3. 简化着色器
   - 帧率提升 → 着色器瓶颈

11.2.2 性能测量 ​

javascript
/**
 * 性能监控类
 */
class PerformanceMonitor {
    constructor() {
        this.frames = 0;
        this.lastTime = performance.now();
        this.fps = 0;
        this.frameTime = 0;
        this.frameTimes = [];
        this.maxSamples = 60;
    }
    
    begin() {
        this.frameStart = performance.now();
    }
    
    end() {
        const now = performance.now();
        const frameTime = now - this.frameStart;
        
        this.frameTimes.push(frameTime);
        if (this.frameTimes.length > this.maxSamples) {
            this.frameTimes.shift();
        }
        
        this.frames++;
        
        // 每秒更新 FPS
        if (now - this.lastTime >= 1000) {
            this.fps = this.frames;
            this.frameTime = this.frameTimes.reduce((a, b) => a + b) / this.frameTimes.length;
            this.frames = 0;
            this.lastTime = now;
        }
    }
    
    getStats() {
        return {
            fps: this.fps,
            frameTime: this.frameTime.toFixed(2),
            minFrameTime: Math.min(...this.frameTimes).toFixed(2),
            maxFrameTime: Math.max(...this.frameTimes).toFixed(2)
        };
    }
}

// 使用
const perfMonitor = new PerformanceMonitor();

function render() {
    perfMonitor.begin();
    
    // 渲染代码...
    
    perfMonitor.end();
    
    requestAnimationFrame(render);
}

// 定期输出性能数据
setInterval(() => {
    console.log(perfMonitor.getStats());
}, 1000);

11.2.3 GPU 时间测量(WebGL 2.0 扩展) ​

javascript
// 使用 EXT_disjoint_timer_query_webgl2 扩展
const ext = gl.getExtension('EXT_disjoint_timer_query_webgl2');

if (ext) {
    const query = gl.createQuery();
    
    gl.beginQuery(ext.TIME_ELAPSED_EXT, query);
    // 绘制命令...
    gl.endQuery(ext.TIME_ELAPSED_EXT);
    
    // 稍后获取结果(异步)
    function checkQuery() {
        if (gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE)) {
            const timeElapsed = gl.getQueryParameter(query, gl.QUERY_RESULT);
            console.log('GPU 时间:', timeElapsed / 1000000, 'ms');
        } else {
            requestAnimationFrame(checkQuery);
        }
    }
    
    requestAnimationFrame(checkQuery);
}

11.3 Draw Call 优化 ​

11.3.1 减少 Draw Call ​

Draw Call 开销

每次 Draw Call 都有 CPU 开销:
- 驱动程序验证状态
- 命令缓冲区提交
- CPU-GPU 同步

优化策略:
──────────────────────────────────

1. 批处理(Batching)
   将多个小物体合并成一个大的 Draw Call
   
   Before:          After:
   draw(mesh1)  →   draw(combinedMesh)
   draw(mesh2)
   draw(mesh3)


2. 实例化(Instancing)
   相同几何体,不同变换
   
   Before:          After:
   for 1000 次:  →  drawInstanced(mesh, 1000)
     draw(mesh)


3. 纹理图集(Texture Atlas)
   多个纹理合并成一张
   避免纹理切换


4. 排序绘制顺序
   按材质、纹理排序
   减少状态切换

11.3.2 动态批处理实现 ​

javascript
/**
 * 动态批处理器
 */
class DynamicBatcher {
    constructor(gl, maxVertices = 65536) {
        this.gl = gl;
        this.maxVertices = maxVertices;
        
        // 预分配缓冲区
        this.positions = new Float32Array(maxVertices * 3);
        this.normals = new Float32Array(maxVertices * 3);
        this.uvs = new Float32Array(maxVertices * 2);
        this.colors = new Float32Array(maxVertices * 4);
        
        this.vertexCount = 0;
        this.currentTexture = null;
        
        this.positionBuffer = gl.createBuffer();
        this.normalBuffer = gl.createBuffer();
        this.uvBuffer = gl.createBuffer();
        this.colorBuffer = gl.createBuffer();
    }
    
    begin() {
        this.vertexCount = 0;
    }
    
    addMesh(mesh, transform, color = [1, 1, 1, 1]) {
        // 检查是否需要 flush
        if (this.vertexCount + mesh.vertexCount > this.maxVertices) {
            this.flush();
        }
        
        // 检查纹理是否改变
        if (mesh.texture !== this.currentTexture) {
            this.flush();
            this.currentTexture = mesh.texture;
        }
        
        // 添加顶点数据(应用变换)
        for (let i = 0; i < mesh.vertexCount; i++) {
            const vi = this.vertexCount + i;
            const mi = i * 3;
            
            // 变换位置
            const pos = this.transformPoint(
                mesh.positions[mi],
                mesh.positions[mi + 1],
                mesh.positions[mi + 2],
                transform
            );
            
            this.positions[vi * 3] = pos[0];
            this.positions[vi * 3 + 1] = pos[1];
            this.positions[vi * 3 + 2] = pos[2];
            
            // 复制其他属性...
        }
        
        this.vertexCount += mesh.vertexCount;
    }
    
    flush() {
        if (this.vertexCount === 0) return;
        
        const gl = this.gl;
        
        // 上传数据
        gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, this.positions.subarray(0, this.vertexCount * 3), gl.DYNAMIC_DRAW);
        
        // ... 其他缓冲区
        
        // 绑定纹理
        if (this.currentTexture) {
            gl.bindTexture(gl.TEXTURE_2D, this.currentTexture);
        }
        
        // 绘制
        gl.drawArrays(gl.TRIANGLES, 0, this.vertexCount);
        
        this.vertexCount = 0;
    }
    
    end() {
        this.flush();
    }
}

11.3.3 按状态排序 ​

javascript
/**
 * 渲染队列排序
 */
class RenderQueue {
    constructor() {
        this.opaqueQueue = [];
        this.transparentQueue = [];
    }
    
    add(renderable) {
        if (renderable.isTransparent) {
            this.transparentQueue.push(renderable);
        } else {
            this.opaqueQueue.push(renderable);
        }
    }
    
    sort(cameraPosition) {
        // 不透明物体:按材质/纹理排序(减少状态切换)
        this.opaqueQueue.sort((a, b) => {
            // 先按着色器排序
            if (a.shader !== b.shader) {
                return a.shader.id - b.shader.id;
            }
            // 再按纹理排序
            if (a.texture !== b.texture) {
                return a.texture.id - b.texture.id;
            }
            // 最后按距离排序(近到远,利用深度测试提前丢弃)
            return a.distanceTo(cameraPosition) - b.distanceTo(cameraPosition);
        });
        
        // 透明物体:从远到近排序
        this.transparentQueue.sort((a, b) => {
            return b.distanceTo(cameraPosition) - a.distanceTo(cameraPosition);
        });
    }
    
    render() {
        let currentShader = null;
        let currentTexture = null;
        
        // 渲染不透明物体
        for (const obj of this.opaqueQueue) {
            // 只在必要时切换状态
            if (obj.shader !== currentShader) {
                obj.shader.use();
                currentShader = obj.shader;
            }
            if (obj.texture !== currentTexture) {
                obj.texture.bind();
                currentTexture = obj.texture;
            }
            obj.draw();
        }
        
        // 渲染透明物体
        // ...
    }
    
    clear() {
        this.opaqueQueue.length = 0;
        this.transparentQueue.length = 0;
    }
}

11.4 着色器优化 ​

11.4.1 避免分支 ​

glsl
// ❌ 不好:动态分支
if (u_lightType == 0) {
    // 点光源计算
} else if (u_lightType == 1) {
    // 方向光计算
} else {
    // 聚光灯计算
}

// ✅ 好:使用不同的着色器程序
// 为每种光源类型编译专门的着色器

// ✅ 好:使用混合代替分支
float isPoint = float(u_lightType == 0);
float isDir = float(u_lightType == 1);

result = pointLightCalc * isPoint + 
         dirLightCalc * isDir + 
         spotLightCalc * (1.0 - isPoint - isDir);

// ✅ 好:使用 step/smoothstep
// 代替 if (x > threshold)
float factor = step(threshold, x);

11.4.2 精度选择 ​

glsl
// 根据需要选择精度
precision highp float;   // 位置、变换矩阵
precision mediump float; // 颜色、UV
precision lowp float;    // 颜色查找

// 在片段着色器中
// ❌ 全部使用 highp
precision highp float;

// ✅ 按需使用
precision mediump float;  // 默认中精度

highp vec4 worldPos;      // 位置需要高精度
lowp vec4 color;          // 颜色可以低精度

11.4.3 减少计算 ​

glsl
// ❌ 在循环中重复计算
for (int i = 0; i < NUM_LIGHTS; i++) {
    vec3 L = normalize(u_lights[i].position - v_worldPos);  // 每次都归一化
    float NdotL = dot(N, L);
    // ...
}

// ✅ 预计算不变的值
vec3 N = normalize(v_normal);  // 只归一化一次

for (int i = 0; i < NUM_LIGHTS; i++) {
    // ...
}


// ❌ 使用 pow 计算整数幂
float x2 = pow(x, 2.0);
float x3 = pow(x, 3.0);

// ✅ 直接乘法
float x2 = x * x;
float x3 = x * x * x;


// ❌ 计算不需要的值
vec4 result;
result.rgb = texture(u_tex, uv).rgb;
result.a = 1.0;  // alpha 固定为 1
// 但纹理采样读取了 4 个通道

// ✅ 使用 swizzle 或专用纹理
vec3 rgb = texture(u_tex, uv).rgb;

11.4.4 向量化操作 ​

glsl
// ❌ 标量操作
float r = color.r * 0.5;
float g = color.g * 0.5;
float b = color.b * 0.5;

// ✅ 向量操作
vec3 result = color.rgb * 0.5;


// ❌ 分别计算点积分量
float d = a.x * b.x + a.y * b.y + a.z * b.z;

// ✅ 使用内置函数
float d = dot(a, b);

11.5 纹理优化 ​

11.5.1 纹理压缩 ​

javascript
// 检查压缩纹理支持
const s3tc = gl.getExtension('WEBGL_compressed_texture_s3tc');
const etc = gl.getExtension('WEBGL_compressed_texture_etc');
const astc = gl.getExtension('WEBGL_compressed_texture_astc');

// 使用压缩纹理
if (s3tc) {
    gl.compressedTexImage2D(
        gl.TEXTURE_2D,
        0,
        s3tc.COMPRESSED_RGBA_S3TC_DXT5_EXT,
        width, height,
        0,
        compressedData
    );
}

// 压缩格式对比
// DXT (S3TC): PC 常用,不支持移动端
// ETC: Android 支持
// PVRTC: iOS 支持
// ASTC: 高质量,新设备支持

// 使用 Basis Universal 或 KTX2 格式
// 可以转码到目标平台的格式

11.5.2 Mipmap 策略 ​

javascript
// 正确使用 Mipmap
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
gl.generateMipmap(gl.TEXTURE_2D);

// 限制最大 mipmap 级别
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAX_LEVEL, 4);

// 各向异性过滤(提高斜视质量)
const aniso = gl.getExtension('EXT_texture_filter_anisotropic');
if (aniso) {
    const max = gl.getParameter(aniso.MAX_TEXTURE_MAX_ANISOTROPY_EXT);
    gl.texParameterf(gl.TEXTURE_2D, aniso.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(8, max));
}

11.5.3 纹理图集 ​

javascript
/**
 * 纹理图集管理器
 */
class TextureAtlas {
    constructor(gl, width, height) {
        this.gl = gl;
        this.width = width;
        this.height = height;
        
        this.texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, this.texture);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
        
        this.regions = new Map();
        this.packer = new RectanglePacker(width, height);
    }
    
    add(name, image) {
        const region = this.packer.pack(image.width, image.height);
        if (!region) {
            console.error('图集空间不足');
            return null;
        }
        
        const gl = this.gl;
        gl.bindTexture(gl.TEXTURE_2D, this.texture);
        gl.texSubImage2D(
            gl.TEXTURE_2D, 0,
            region.x, region.y,
            gl.RGBA, gl.UNSIGNED_BYTE,
            image
        );
        
        // 存储 UV 坐标
        const uvRegion = {
            u1: region.x / this.width,
            v1: region.y / this.height,
            u2: (region.x + image.width) / this.width,
            v2: (region.y + image.height) / this.height
        };
        
        this.regions.set(name, uvRegion);
        return uvRegion;
    }
    
    getUV(name) {
        return this.regions.get(name);
    }
}

11.6 内存管理 ​

11.6.1 资源清理 ​

javascript
/**
 * 资源管理器
 */
class ResourceManager {
    constructor(gl) {
        this.gl = gl;
        this.textures = new Map();
        this.buffers = new Map();
        this.programs = new Map();
        this.framebuffers = new Map();
    }
    
    createTexture(name, image) {
        const gl = this.gl;
        const texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
        gl.generateMipmap(gl.TEXTURE_2D);
        
        this.textures.set(name, texture);
        return texture;
    }
    
    deleteTexture(name) {
        const texture = this.textures.get(name);
        if (texture) {
            this.gl.deleteTexture(texture);
            this.textures.delete(name);
        }
    }
    
    // 清理所有资源
    dispose() {
        const gl = this.gl;
        
        for (const texture of this.textures.values()) {
            gl.deleteTexture(texture);
        }
        
        for (const buffer of this.buffers.values()) {
            gl.deleteBuffer(buffer);
        }
        
        for (const program of this.programs.values()) {
            gl.deleteProgram(program);
        }
        
        for (const fb of this.framebuffers.values()) {
            gl.deleteFramebuffer(fb);
        }
        
        this.textures.clear();
        this.buffers.clear();
        this.programs.clear();
        this.framebuffers.clear();
    }
}

11.6.2 对象池 ​

javascript
/**
 * 对象池 - 避免频繁创建和销毁
 */
class ObjectPool {
    constructor(createFn, resetFn, initialSize = 100) {
        this.createFn = createFn;
        this.resetFn = resetFn;
        this.pool = [];
        
        // 预创建
        for (let i = 0; i < initialSize; i++) {
            this.pool.push(createFn());
        }
    }
    
    acquire() {
        if (this.pool.length > 0) {
            return this.pool.pop();
        }
        return this.createFn();
    }
    
    release(obj) {
        this.resetFn(obj);
        this.pool.push(obj);
    }
}

// 使用示例:粒子池
const particlePool = new ObjectPool(
    () => ({ x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, life: 0 }),
    (p) => { p.x = p.y = p.z = p.vx = p.vy = p.vz = p.life = 0; }
);

// 获取粒子
const particle = particlePool.acquire();

// 归还粒子
particlePool.release(particle);

11.7 常见性能问题 ​

11.7.1 问题清单 ​

常见性能问题及解决方案

1. 过多的 Draw Call
──────────────────────────────────
症状:CPU 使用率高,GPU 空闲
解决:批处理、实例化、合并网格


2. 着色器编译卡顿
──────────────────────────────────
症状:首次渲染时卡顿
解决:预编译着色器、使用着色器缓存


3. 纹理上传阻塞
──────────────────────────────────
症状:加载新纹理时卡顿
解决:异步加载、渐进式加载、使用 ImageBitmap


4. 过度绘制(Overdraw)
──────────────────────────────────
症状:GPU 片段处理瓶颈
解决:从前到后渲染、遮挡剔除、LOD


5. 内存泄漏
──────────────────────────────────
症状:内存持续增长
解决:正确删除资源、使用资源管理器


6. GC 停顿
──────────────────────────────────
症状:周期性卡顿
解决:对象池、避免临时对象

11.7.2 异步纹理加载 ​

javascript
/**
 * 异步纹理加载
 */
async function loadTextureAsync(gl, url) {
    // 使用 ImageBitmap 进行离线解码
    const response = await fetch(url);
    const blob = await response.blob();
    const imageBitmap = await createImageBitmap(blob);
    
    const texture = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, texture);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imageBitmap);
    gl.generateMipmap(gl.TEXTURE_2D);
    
    imageBitmap.close();  // 释放 ImageBitmap
    
    return texture;
}

// 批量加载
async function loadTextures(gl, urls) {
    const promises = urls.map(url => loadTextureAsync(gl, url));
    return Promise.all(promises);
}

11.8 调试工具 ​

11.8.1 Spector.js ​

javascript
// 使用 Spector.js 捕获帧
// 1. 安装 Chrome 扩展或通过 npm
// npm install spectorjs

import { Spector } from 'spectorjs';

const spector = new Spector();

// 捕获下一帧
spector.captureNextFrame(canvas);

// 或手动捕获
spector.startCapture(canvas, 100);  // 捕获 100 个命令
// ... 渲染
spector.stopCapture();

// 查看捕获结果
spector.onCapture.add((capture) => {
    console.log('捕获的命令:', capture.commands);
    console.log('状态:', capture.contexts);
});

11.8.2 Chrome DevTools ​

javascript
// 使用 console 分组输出
console.group('Frame');
console.log('Draw calls:', drawCallCount);
console.log('Triangles:', triangleCount);
console.groupEnd();

// 性能标记
performance.mark('render-start');
// 渲染代码
performance.mark('render-end');
performance.measure('render', 'render-start', 'render-end');

// 获取测量结果
const measures = performance.getEntriesByName('render');
console.log('渲染时间:', measures[measures.length - 1].duration, 'ms');

11.8.3 WebGL 错误检查 ​

javascript
/**
 * WebGL 错误检查
 */
function checkGLError(gl, operation = '') {
    const error = gl.getError();
    if (error !== gl.NO_ERROR) {
        const errorNames = {
            [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'
        };
        console.error(`WebGL 错误 ${operation}: ${errorNames[error] || error}`);
        return false;
    }
    return true;
}

// 开发模式下检查每个操作
function debugGL(gl) {
    const originalFunctions = {};
    
    for (const key of Object.keys(gl.__proto__)) {
        if (typeof gl[key] === 'function') {
            originalFunctions[key] = gl[key];
            
            gl[key] = function(...args) {
                const result = originalFunctions[key].apply(gl, args);
                checkGLError(gl, key);
                return result;
            };
        }
    }
}

11.8.4 着色器调试 ​

glsl
// 可视化调试

// 可视化法线
gl_FragColor = vec4(v_normal * 0.5 + 0.5, 1.0);

// 可视化 UV
gl_FragColor = vec4(v_texCoord, 0.0, 1.0);

// 可视化深度
float depth = gl_FragCoord.z;
gl_FragColor = vec4(vec3(depth), 1.0);

// 可视化世界位置
gl_FragColor = vec4(fract(v_worldPos), 1.0);

// 热力图可视化(用于显示计算量)
// 根据某个值映射到颜色
vec3 heatmap(float t) {
    return vec3(
        clamp(1.5 - abs(4.0 * t - 3.0), 0.0, 1.0),
        clamp(1.5 - abs(4.0 * t - 2.0), 0.0, 1.0),
        clamp(1.5 - abs(4.0 * t - 1.0), 0.0, 1.0)
    );
}

11.9 本章小结 ​

优化策略总结 ​

领域优化方法
Draw Call批处理、实例化、排序
着色器避免分支、适当精度、向量化
纹理压缩、Mipmap、图集
内存资源管理、对象池、及时清理
加载异步加载、渐进式

调试工具 ​

工具用途
Spector.js捕获和分析 WebGL 调用
Chrome DevTools性能分析、内存检查
RenderDocGPU 调试(需要特殊构建)

11.10 练习题 ​

基础练习 ​

  1. 实现性能监控,显示 FPS 和帧时间

  2. 使用实例化渲染优化大量相同物体的绘制

进阶练习 ​

  1. 实现简单的批处理系统

  2. 比较不同精度和优化级别的着色器性能

挑战练习 ​

  1. 使用 Spector.js 分析和优化一个复杂场景

下一章预告:在第12章中,我们将通过完整的实战案例,综合运用所学知识。


文档版本:v1.0
字数统计:约 11,000 字
代码示例:45+ 个

如有转载或 CV 的请标注本站原文地址