WebGL 性能优化有哪些常用技巧?
减少 Draw Call
Draw Call 是 CPU 向 GPU 提交绘制命令的过程,每次调用都有固定开销,是最常见的性能瓶颈。
批量绘制(Batching):将使用相同着色器的多个网格合并到一个缓冲区,用一次 drawArrays 替代多次调用。适合静态场景中的同类物体。
实例化渲染(Instanced Rendering):WebGL 2.0 原生支持,适合渲染大量相同几何体(如森林中的树木、粒子系统):
javascriptgl.drawArraysInstanced(gl.TRIANGLES, 0, vertexCount, instanceCount);
实例化通过 gl.vertexAttribDivisor 让每个实例读取不同的属性(位置、颜色等),一次 Draw Call 完成。
关键指标:移动端 Draw Call 超过 100 次就可能卡顿,桌面端建议控制在 2000 次以内。
减少状态切换
每次切换着色器程序、纹理、Blend 模式等状态都会触发 GPU 管线刷新。优化思路:
- 按状态排序:先按着色器分组,再按纹理分组,减少
gl.useProgram和gl.bindTexture调用次数 - 纹理图集(Texture Atlas):将多张小纹理合并为一张大图,通过 UV 偏移采样,只需绑定一次纹理
- VAO(Vertex Array Object):WebGL 2.0 原生支持,将顶点属性配置缓存到 VAO 中,切换时只需
gl.bindVertexArray(vao),替代逐个设置vertexAttribPointer
着色器优化
CPU 预计算:将 projection * view * model 矩阵在 JS 端乘好再传入着色器,避免每个顶点重复计算:
glsluniform mat4 u_mvpMatrix; void main() { gl_Position = u_mvpMatrix * vec4(a_position, 1.0); }
精度控制:移动端 GPU 对精度敏感,合理使用精度修饰符可提升 2-5 倍性能:
glslhighp mat4 u_mvpMatrix; // 变换矩阵必须高精度 mediump vec2 a_texCoord; // 纹理坐标中精度足够 lowp vec4 v_color; // 颜色低精度即可
移动端片段着色器默认 mediump float 即可,盲目使用 highp 会显著拖慢渲染。
将计算移至顶点着色器:光照计算从片段着色器移到顶点着色器,利用硬件插值,减少逐像素计算量。
减少分支:GPU 是 SIMT 架构,动态分支(if/else)会导致同一 warp 内线程分叉执行,性能骤降。用 mix、step 等内置函数替代条件判断。
几何与缓冲区优化
索引绘制:用 gl.drawElements + 索引缓冲区复用顶点。一个立方体从 36 个顶点减少到 8 个顶点 + 36 个索引,顶点数据减少约 75%。
交错顶点数据(Interleaved Arrays):将 position、color、texCoord 打包到一个缓冲区,比分离缓冲区缓存命中率更高:
javascript// x,y,z, r,g,b, u,v 交错排列 const vertices = new Float32Array([ 0,0,0, 1,0,0, 0,0, 1,0,0, 0,1,0, 1,0, ]);
Draco 压缩:对 glTF 模型使用 Google Draco 压缩,典型压缩率可达 10:1,大幅减少加载时间和内存占用。
纹理优化
纹理压缩:使用 GPU 原生支持的压缩格式(S3TC/DXT for 桌面、ETC2 for Android、ASTC for iOS),减少 75% 显存占用:
javascriptconst ext = gl.getExtension("WEBGL_compressed_texture_s3tc"); gl.compressedTexImage2D(gl.TEXTURE_2D, 0, ext.COMPRESSED_RGBA_S3TC_DXT5_EXT, w, h, 0, data);
Mipmap:对会缩小的纹理开启 Mipmap,LINEAR_MIPMAP_LINEAR 过滤既提升渲染质量又减少纹理采样带宽。注意 2 的幂次尺寸才能自动生成 Mipmap。
LOD(Level of Detail):根据物体与相机距离切换不同精度的纹理和模型,远距离用低精度资源,近处用高精度。
剔除策略
视锥体剔除(Frustum Culling):用包围盒做快速判定,完全在视锥外的物体直接跳过,避免提交 GPU:
javascriptfor (const obj of scene.objects) { if (isInFrustum(obj.boundingBox, vpMatrix)) { obj.render(); } }
背面剔除:gl.enable(gl.CULL_FACE) 一行代码即可剔除背向相机的三角形,减少约 50% 的片元处理量。
遮挡查询:WebGL 2.0 的 gl.beginQuery(gl.ANY_SAMPLES_PASSED, query) 可判断物体是否被遮挡,被遮挡则跳过精细渲染。
从前往后排序:不透明物体按距相机由近到远绘制,利用 Early-Z 测试减少 overdraw。
分辨率与帧缓冲区
DPR 限制:高 DPI 屏幕上不必按完整 devicePixelRatio 渲染:
javascriptconst dpr = Math.min(window.devicePixelRatio, 2); // 上限 2x canvas.width = canvas.clientWidth * dpr;
动态降分辨率:帧率低于阈值时自动降低渲染分辨率,用 CSS 缩放回原始尺寸,用户几乎无感知但帧率可提升 30-50%。
延迟渲染 G-Buffer 精度:位置用 RGBA16F,法线用 RGB10_A2,材质用 RGBA8,避免全部高精度浪费带宽。
JavaScript 层优化
避免 GC:渲染循环内不创建新对象,预分配 Float32Array 复用:
javascriptconst matrix = new Float32Array(16); function update() { mat4.identity(matrix); // 复用 }
OffscreenCanvas + WebWorker:将渲染逻辑移至 Worker 线程,避免阻塞主线程 UI 响应:
javascriptconst offscreen = canvas.transferControlToOffscreen(); const worker = new Worker("renderer.js"); worker.postMessage({ canvas: offscreen }, [offscreen]);
及时释放资源:不再使用的纹理、缓冲区立即 gl.deleteTexture / gl.deleteBuffer,避免显存泄漏。
性能监控
GPU 计时:EXT_disjoint_timer_query 扩展可精确测量 GPU 执行时间,定位渲染瓶颈:
javascriptconst ext = gl.getExtension("EXT_disjoint_timer_query"); const query = ext.createQueryEXT(); ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, query); drawScene(); ext.endQueryEXT(ext.TIME_ELAPSED_EXT);
Chrome DevTools:Performance 面板可分析帧时间分布,判断瓶颈在 CPU(JS 逻辑)还是 GPU(渲染管线)。
Spector.js:浏览器扩展,逐帧记录所有 WebGL 调用,直观发现冗余状态切换和 Draw Call 问题。
面试回答要点
回答 WebGL 性能优化时,按优先级组织:先说 Draw Call 和状态切换(最常见瓶颈),再说着色器优化和几何优化(GPU 侧),最后说 JS 层和监控工具。关键是展示系统性思维——知道瓶颈在哪、用什么工具定位、用什么方案解决。实际项目中,90% 的性能问题来自 Draw Call 过多和 overdraw,优先解决这两个问题通常就能获得显著提升。