WebGL Performance Optimization Overview
WebGL performance optimization is crucial for 3D Web application development. Due to communication overhead between JavaScript and GPU, and resource limitations on mobile devices, proper optimization strategies can significantly improve rendering performance.
1. Reduce Draw Calls
Problem
Each gl.drawArrays or gl.drawElements has CPU-to-GPU communication overhead.
Optimization Solutions
Batching
javascript// Before optimization: multiple draw calls for (let mesh of meshes) { gl.bindBuffer(gl.ARRAY_BUFFER, mesh.vbo); gl.drawArrays(gl.TRIANGLES, 0, mesh.vertexCount); } // After optimization: merge into single buffer const mergedBuffer = mergeMeshes(meshes); gl.bindBuffer(gl.ARRAY_BUFFER, mergedBuffer); gl.drawArrays(gl.TRIANGLES, 0, totalVertexCount);
Instanced Rendering
javascript// WebGL 2.0 native support // Single draw call renders multiple identical geometries const instanceCount = 1000; gl.drawArraysInstanced(gl.TRIANGLES, 0, vertexCount, instanceCount);
2. Reduce State Changes
Problem
Frequent switching of shader programs, textures, buffers, and other states causes performance overhead.
Optimization Solutions
Sort by State
javascript// Sort by shader program meshes.sort((a, b) => a.program.id - b.program.id); // Sort by texture meshes.sort((a, b) => a.texture.id - b.texture.id); let currentProgram = null; let currentTexture = null; for (let mesh of meshes) { // Only switch program when necessary if (mesh.program !== currentProgram) { gl.useProgram(mesh.program); currentProgram = mesh.program; } // Only switch texture when necessary if (mesh.texture !== currentTexture) { gl.bindTexture(gl.TEXTURE_2D, mesh.texture); currentTexture = mesh.texture; } mesh.draw(); }
Use Texture Atlas
javascript// Merge multiple small textures into one large texture // Reduces texture binding switch count const atlasTexture = createTextureAtlas([ 'texture1.png', 'texture2.png', 'texture3.png' ]); // Use texture coordinate offset in shader uniform vec2 u_atlasOffset; uniform vec2 u_atlasScale; vec2 atlasCoord = a_texCoord * u_atlasScale + u_atlasOffset; vec4 color = texture2D(u_texture, atlasCoord);
3. Optimize Shaders
Vertex Shader Optimization
glsl// Before optimization: complex calculations in vertex shader attribute vec3 a_position; uniform mat4 u_modelMatrix; uniform mat4 u_viewMatrix; uniform mat4 u_projectionMatrix; void main() { // Matrix multiplication for each vertex mat4 mvp = u_projectionMatrix * u_viewMatrix * u_modelMatrix; gl_Position = mvp * vec4(a_position, 1.0); } // After optimization: pre-compute MVP matrix on CPU uniform mat4 u_mvpMatrix; void main() { gl_Position = u_mvpMatrix * vec4(a_position, 1.0); }
Fragment Shader Optimization
glsl// Before optimization: complex per-pixel calculations void main() { vec3 lightDir = normalize(u_lightPos - v_worldPos); float diff = max(dot(v_normal, lightDir), 0.0); vec3 diffuse = diff * u_lightColor; // ... more calculations } // After optimization: compute lighting in vertex shader // Vertex shader varying vec3 v_lightIntensity; void main() { // Calculate lighting at vertex level vec3 lightDir = normalize(u_lightPos - worldPos); float diff = max(dot(normal, lightDir), 0.0); v_lightIntensity = diff * u_lightColor; } // Fragment shader void main() { // Use interpolated lighting intensity gl_FragColor = vec4(v_lightIntensity * textureColor, 1.0); }
Use Appropriate Precision
glsl// High precision (highp) - vertex positions, transformation matrices attribute highp vec3 a_position; uniform highp mat4 u_mvpMatrix; // Medium precision (mediump) - colors, texture coordinates attribute mediump vec2 a_texCoord; varying mediump vec2 v_texCoord; // Low precision (lowp) - lighting calculation results varying lowp vec3 v_lightColor;
4. Buffer Optimization
Use VAO to Reduce State Setup
javascript// WebGL 2.0 or extension support const vao = gl.createVertexArray(); gl.bindVertexArray(vao); // Configure vertex attributes (execute once) gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(0); gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer); gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(1); gl.bindVertexArray(null); // When drawing, just bind VAO gl.bindVertexArray(vao); gl.drawArrays(gl.TRIANGLES, 0, count);
Use Indexed Drawing
javascript// Before optimization: 36 vertices define a cube const vertices = new Float32Array([ // 6 vertices per face, 6 faces // Lots of duplicate vertex data ]); // After optimization: 8 vertices + 36 indices const vertices = new Float32Array([ // 8 unique vertices ]); const indices = new Uint16Array([ // 36 indices define 12 triangles ]); gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);
Use Interleaved Arrays
javascript// Before optimization: separate buffers const positions = new Float32Array([/* ... */]); const colors = new Float32Array([/* ... */]); const texCoords = new Float32Array([/* ... */]); // After optimization: interleaved vertex data const vertices = new Float32Array([ // x, y, z, r, g, b, u, v 0, 0, 0, 1, 0, 0, 0, 0, // vertex 1 1, 0, 0, 0, 1, 0, 1, 0, // vertex 2 // ... ]); // Better cache locality
5. Texture Optimization
Texture Compression
javascript// Use compressed texture formats const compressedExtension = gl.getExtension('WEBGL_compressed_texture_s3tc'); // Upload compressed texture data gl.compressedTexImage2D( gl.TEXTURE_2D, 0, compressedExtension.COMPRESSED_RGBA_S3TC_DXT5_EXT, width, height, 0, compressedData );
Mipmap Usage
javascript// Enable mipmap for better quality and performance gl.generateMipmap(gl.TEXTURE_2D); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
Texture Size Optimization
- Use power-of-2 dimensions (supports mipmap)
- Avoid overly large textures (memory and bandwidth overhead)
- Use different resolution textures based on distance (LOD)
6. Occlusion and Frustum Culling
Frustum Culling
javascriptfunction isInFrustum(boundingBox, viewProjectionMatrix) { // Transform bounding box to clip space // Check if inside frustum const corners = boundingBox.getCorners(); for (let corner of corners) { const clipPos = transformPoint(corner, viewProjectionMatrix); if (Math.abs(clipPos.x) <= clipPos.w && Math.abs(clipPos.y) <= clipPos.w && 0 <= clipPos.z && clipPos.z <= clipPos.w) { return true; } } return false; } // Only render objects inside frustum for (let object of scene.objects) { if (isInFrustum(object.boundingBox, vpMatrix)) { object.render(); } }
Occlusion Query (WebGL 2.0)
javascriptconst query = gl.createQuery(); // Start occlusion query gl.beginQuery(gl.ANY_SAMPLES_PASSED, query); drawBoundingBox(object); gl.endQuery(gl.ANY_SAMPLES_PASSED); // Check result const available = gl.getQueryParameter(query, gl.QUERY_RESULT_AVAILABLE); if (available) { const visible = gl.getQueryParameter(query, gl.QUERY_RESULT) > 0; if (visible) { drawDetailedMesh(object); } }
7. Framebuffer Optimization
Reduce Resolution
javascript// Use appropriate resolution on high DPI screens const dpr = Math.min(window.devicePixelRatio, 2); canvas.width = canvas.clientWidth * dpr; canvas.height = canvas.clientHeight * dpr;
Deferred Rendering Optimization
javascript// G-Buffer optimization: use appropriate precision // Position: RGB16F or RGBA16F // Normal: RGB10_A2 or RGBA8 // Material: RGBA8
8. JavaScript Optimization
Avoid Garbage Collection
javascript// Before optimization: create new array every frame function update() { const matrix = new Float32Array(16); // Creates garbage // ... } // After optimization: reuse arrays const matrix = new Float32Array(16); function update() { // Reuse matrix, don't create new objects mat4.identity(matrix); // ... }
Use TypedArrays
javascript// Use Float32Array instead of regular arrays const positions = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]); gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
9. Mobile Optimization
Reduce Overdraw
javascript// Draw opaque objects front-to-back gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); opaqueObjects.sort((a, b) => b.distance - a.distance); for (let obj of opaqueObjects) { obj.draw(); }
Use Appropriate Precision
glsl// Use mediump on mobile for better performance precision mediump float;
Avoid Complex Shaders
- Reduce texture sampling count
- Avoid dynamic branching
- Simplify lighting calculations
10. Performance Monitoring
javascript// Use EXT_disjoint_timer_query to measure GPU time const 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); // Get result const available = ext.getQueryObjectEXT(query, ext.QUERY_RESULT_AVAILABLE_EXT); if (available) { const timeElapsed = ext.getQueryObjectEXT(query, ext.QUERY_RESULT_EXT); console.log(`GPU time: ${timeElapsed / 1000000} ms`); }
Summary
| Optimization Direction | Main Techniques |
|---|---|
| Draw Calls | Batching, Instanced Rendering |
| State Changes | State sorting, Texture atlas, VAO |
| Shaders | Pre-computation, Appropriate precision, Simplify calculations |
| Buffers | Indexed drawing, Interleaved arrays |
| Textures | Compression, Mipmap, Reasonable sizes |
| Culling | Frustum culling, Occlusion queries |
| JavaScript | Avoid GC, TypedArrays |