用 WebGPU 解锁 GPU 计算能力
Unlock GPU computing with WebGPU
2025年6月9日
一句话判断
WebGPU 在 Apple 平台上几乎是一对一映射 Metal 的——如果你熟悉 Metal,上手 WebGPU 的障碍接近于零;如果你只熟悉 WebGL,那 compute shader 的能力会让你重新思考”浏览器里能做什么”。
这场 Session 讲了什么
WebGPU 不仅是 WebGL 的升级版,更是浏览器中唯一能做通用 GPU 计算的 API。Session 从三个方面展开:API 概览、着色器开发、性能优化。
API 层面,WebGPU 是一个扁平的接口体系,核心概念和 Metal 高度对齐:GPUDevice 对应 MTLDevice,GPUBindGroup 对应 Metal 的 argument buffer,render/compute pipeline 对应 MTLRenderPipelineState/MTLComputePipelineState。着色语言 WGSL 是从零设计的安全着色语言,支持 vertex、fragment 和 compute 三种程序类型。Session 用一个粒子动画示例展示了从顶点着色、片段着色到 compute shader 物理模拟的完整流程。
性能优化部分是亮点:使用 f16(half-precision float)减少内存带宽压力;用 render bundle 缓存绘制命令避免每帧重复验证;用 dynamic offset 合并 bind group 减少 Metal 对象数量;尽量合并 command buffer 和 pass 以减少同步开销。
值得深挖的点
Compute shader:WebGL 做不到的事
WebGL 不支持 compute shader,这意味着在浏览器中做通用 GPU 计算(机器学习推理、物理模拟、数据处理)以前是不可能的。WebGPU 的 compute shader 打开了这扇门。Session 中的粒子系统示例:100,000 个粒子的物理模拟(重力、速度、衰减)全部在 GPU 上完成,结果写入 buffer,JavaScript 通过 readBuffer 读取。
这和 Web Workers 做并行计算有本质区别:GPU 的并行粒度是 thousands of threads,远超 CPU 核心数。对于有大量相同计算但不同数据的场景(矩阵运算、图像处理、模拟),compute shader 可以做到 JavaScript 无法企及的性能。Three.js 等库已经内置了 WebGPU 后端,如果你的需求是 3D 渲染,用高层库就能直接受益。
Apple 平台上的性能细节
Session 特别强调了 Apple 平台(tile-based deferred renderer)上的最佳实践。f16 不只是”节省一半内存”那么简单——在 iOS 和 visionOS 上,使用 f16 或压缩格式可以显著降低内存压力,避免系统终止你的进程。即使只是用 f16 存储数据再立即解包到 f32,仍然能获得内存收益。
render bundle 的价值在于它把验证从每帧执行推迟到创建时执行一次。WebGPU 需要确保所有读写都在边界内,这通常意味着每帧大量验证。但 render bundle 创建时验证一次,之后重复执行。底层映射到 Metal 的 indirect command buffer,性能收益是实打实的。
dynamic offset 是另一个减少 Metal 对象的技巧。与其为每个实例创建一个 64 字节的 bind group(10 个实例 = 10 个 Metal buffer),不如创建一个 640 字节的大 buffer,用 dynamic offset 在 setBindGroup 时切换。这样只创建一个 Metal buffer,节省 9 个。
代码片段
初始化 WebGPU 设备和 canvas:
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice({
requiredFeatures: ['shader-f16'] // 启用半精度浮点
});
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('webgpu');
ctx.configure({ device, format: 'bgra8unorm' });
创建 compute pipeline 和粒子 buffer:
const shaderModule = device.createShaderModule({
code: `
@group(0) @binding(0) var<storage, read_write> particles: array<Particle>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3u) {
var p = particles[id.x];
p.velocity.y -= 0.001; // 重力
p.position += p.velocity;
particles[id.x] = p;
}
`
});
const pipeline = device.createComputePipeline({
layout: 'auto',
compute: { module: shaderModule, entryPoint: 'main' }
});
用 dynamic offset 合并 bind group:
// 一个大 buffer 替代多个小 buffer
const buffer = device.createBuffer({
size: 640, // 10 × 64 字节
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});
// 绘制时通过 offset 切换实例数据
pass.setBindGroup(0, bindGroup, [instanceIndex * 64]);
最佳实践
建议新项目优先使用 WebGPU 而不是 WebGL。WebGPU 在所有 Apple 平台上都通过 Metal 实现,性能和功能都优于 WebGL。如果你的应用对性能敏感,请从一开始就考虑使用 f16 数据类型。
优先使用 render bundle 缓存重复的绘制命令。这在渲染静态或半静态内容(UI 元素、不频繁更新的 3D 场景)时收益最大。
避免频繁更新 index/indirect buffer。这类 buffer 的更新需要额外的边界验证,成本比普通 buffer 高。只在需要时更新,优先使用 read-only 访问模式。
还有什么值得关注
shader-f16扩展在所有 Apple 设备上都可用,但属于可选特性,在其他平台上需要检查支持情况。- WebGPU 通过 Metal 实现,支持 Mac、iPhone、iPad 和 Vision Pro。
- three.js 已经支持 WebGPU 后端,可以直接用来做 3D 渲染而不需要自己写底层代码。