WebGPU 入门:在浏览器中释放 GPU 计算能力
引言
WebGPU 是新一代的 Web 图形和计算 API,旨在取代 WebGL,为浏览器带来更强大的 GPU 访问能力。经过多年的标准化工作和浏览器厂商的打磨,WebGPU 已经在主流浏览器中全面落地。它不仅意味着更好的 3D 渲染性能,更意味着前端开发者可以直接在浏览器中利用 GPU 进行通用计算(GPGPU),这为 AI 推理、科学可视化、图像处理等场景打开了全新的大门。
本文将从零开始,带你理解 WebGPU 的核心概念,并通过实际代码示例展示如何在浏览器中释放 GPU 的计算 power。
WebGPU vs WebGL:不只是升级
WebGL 本质上是 OpenGL ES 的浏览器封装,诞生于 2011 年,设计理念受限于当时的 GPU 架构和 Web 技术水平。WebGPU 则是完全重新设计的现代图形 API,对标 Vulkan、Metal 和 Direct3D 12,更贴近当代 GPU 硬件的工作方式。
核心差异包括:
- 更低的 CPU 开销:WebGPU 采用了显式的 API 设计,状态管理和资源绑定更加清晰,减少了驱动层的隐藏开销
- 计算着色器原生支持:WebGL 只能通过 Hack 的方式(如使用顶点着色器做计算)实现 GPGPU,而 WebGPU 原生提供 Compute Shader 管线
- 更现代的着色器语言:WGSL(WebGPU Shading Language)专为 Web 设计,比 GLSL 更安全、更易维护
- 跨平台一致性:WebGPU 在底层映射到 Vulkan(Windows/Linux/Android)、Metal(macOS/iOS)和 D3D12(Windows),对开发者暴露统一的接口
核心概念解析
1. Adapter 与 Device
WebGPU 的入口是 navigator.gpu 对象。首先需要请求一个 Adapter(代表一个可用的 GPU 适配器),然后从 Adapter 创建一个 Device(逻辑设备,是你与 GPU 交互的上下文):
// 请求 GPU 适配器
const adapter = await navigator.gpu.requestAdapter({
powerPreference: 'high-performance'
});
if (!adapter) {
console.error('当前环境不支持 WebGPU');
return;
}
// 创建逻辑设备
const device = await adapter.requestDevice();
console.log('GPU 设备已就绪:', adapter.info);
Device 是 WebGPU 中资源管理和错误处理的核心单元。所有缓冲区、纹理、着色器模块都通过 Device 创建。
2. 缓冲区(Buffer)
GPU 缓冲区是存储数据的连续内存块。WebGPU 中的 Buffer 需要明确指定用途(usage),这驱动了 GPU 内存布局的优化:
// 创建一个存储浮点数的缓冲区
const buffer = device.createBuffer({
size: 1024, // 字节数,必须是 4 的倍数
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
});
注意 WebGPU 的一个重要设计原则:数据映射。你无法直接读写 GPU 缓冲区,需要通过 writeBuffer / readBuffer 或映射(mapAsync)来进行 CPU-GPU 数据交换。
3. 着色器模块与管线
WGSL 着色器代码通过 createShaderModule 编译为着色器模块,然后用于创建渲染管线或计算管线:
const shaderModule = device.createShaderModule({
code: `
@group(0) @binding(0) var<storage, read_write> data: array<f32>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
if (id.x >= arrayLength(&data)) {
return;
}
data[id.x] = data[id.x] * 2.0; // 每个元素翻倍
}
`
});
4. 绑定组(Bind Group)
绑定组描述了着色器如何访问资源。它是 WebGPU 资源绑定的核心抽象:
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: buffer }
}]
});
实战:用 Compute Shader 做向量乘法
让我们用一个实际示例来串联所有概念。我们将实现一个简单的向量逐元素乘法——将两个数组的对应元素相乘,结果存入第三个数组。
Step 1: 定义 WGSL 着色器
const shaderCode = `
@group(0) @binding(0) var<storage, read> a: array<f32>;
@group(0) @binding(1) var<storage, read> b: array<f32>;
@group(0) @binding(2) var<storage, read_write> result: array<f32>;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let i = gid.x;
if (i >= arrayLength(&a)) {
return;
}
result[i] = a[i] * b[i];
}
`;
Step 2: 初始化数据与缓冲区
const N = 1024;
const aData = new Float32Array(N).map((_, i) => Math.random());
const bData = new Float32Array(N).map((_, i) => Math.random());
// 创建缓冲区
const aBuffer = device.createBuffer({
size: N * 4,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
});
const bBuffer = device.createBuffer({
size: N * 4,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
});
const resultBuffer = device.createBuffer({
size: N * 4,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC
});
// 写入数据
device.queue.writeBuffer(aBuffer, 0, aData);
device.queue.writeBuffer(bBuffer, 0, bData);
Step 3: 创建计算管线并提交
const shader = device.createShaderModule({ code: shaderCode });
const pipeline = device.createComputePipeline({
layout: 'auto',
compute: { module: shader, entryPoint: 'main' }
});
// 创建绑定组
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: aBuffer }},
{ binding: 1, resource: { buffer: bBuffer }},
{ binding: 2, resource: { buffer: resultBuffer }}
]
});
// 提交计算命令
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(Math.ceil(N / 64));
pass.end();
device.queue.submit([encoder.finish()]);
Step 4: 读取结果
// 创建一个可映射的暂存缓冲区
const stagingBuffer = device.createBuffer({
size: N * 4,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
});
const encoder2 = device.createCommandEncoder();
encoder2.copyBufferToBuffer(resultBuffer, 0, stagingBuffer, 0, N * 4);
device.queue.submit([encoder2.finish()]);
await stagingBuffer.mapAsync(GPUMapMode.READ);
const result = new Float32Array(stagingBuffer.getMappedRange());
console.log('前5个结果:', Array.from(result.slice(0, 5)));
stagingBuffer.unmap();
这段代码完整地展示了从数据准备到结果读取的全流程。可以看到,WebGPU 的 API 是显式的——你需要手动管理资源、布局和命令提交,但这也意味着更大的控制力和更好的性能。
性能考量与最佳实践
1. 最小化 CPU-GPU 数据传输
PCIe 总线带宽是瓶颈。尽量将数据持久化在 GPU 端,只在必要时进行读写。多帧之间可以复用缓冲区,避免反复创建和销毁。
2. 合理选择 Workgroup 大小
@workgroup_size 决定了每个工作组的线程数。64 是一个常见的安全值,大多数 GPU 的子组(subgroup)大小为 16-64。太小会导致调度开销,太大可能超出硬件限制。
3. 使用同步原语避免竞态条件
当多个工作组的线程访问同一资源时,需要使用 atomicLoad / atomicStore / atomicAdd 等 WGSL 原子操作来保证正确性。这与 CPU 端的多线程编程类似。
4. 管线缓存
创建管线是一个昂贵操作。尽量复用管线和绑定组布局。WebGPU 也支持通过 device.pipelineCache(在支持的浏览器中)缓存已编译的管线,减少启动时间。
WebGPU 在 AI 推理中的应用
WebGPU 的计算能力使其成为浏览器端 AI 推理的理想后端。实际上,TensorFlow.js 团队和媒体团队的 transformers.js 都已经提供了 WebGPU 后端支持,在兼容设备上可以实现显著的推理加速。
典型场景包括:
- 端侧大模型推理:小规模 LLM(如 1B-3B 参数)可以在较新的设备上实现交互式推理
- 图像处理:卷积运算天然适合 GPU 并行计算
- 物理模拟:粒子系统、流体模拟等需要大规模并行计算的场景
- 音频处理:实时 DSP 效果、频谱分析
OnnxRuntime Web 已经率先集成了 WebGPU EP(执行提供者),可以直接将 ONNX 模型运行在 WebGPU 上。而 WebLLM 项目则进一步展示了纯前端运行 LLM 的可能性——模型权重全部下载到浏览器,推理完全在 GPU 上完成,无需任何服务器调用。
当前生态与兼容性
截至 2026 年中,WebGPU 的浏览器支持情况:
- Chrome/Edge 113+:完整支持(桌面端稳定)
- Safari 18+:完整支持(macOS 和 iOS)
- Firefox:逐步铺开,目前在 Nightly 可用
- 移动端:Android Chrome 已支持,iOS Safari 18 起支持
值得关注的几个生态项目:
- Dawn:Google 的 WebGPU 实现,也是 Chrome 的后端
- wgpu:Rust 实现的 WebGPU,广泛用于 Rust 生态和 WASM 项目
- Babylon.js / Three.js:主流 3D 引擎已适配 WebGPU 渲染管线
- Orillusion:国产 WebGPU 优先的 3D 引擎
总结与展望
WebGPU 不只是 WebGL 的替代品,它是 Web 平台向 GPU 计算时代的一次重要飞跃。从渲染到计算,从 3D 图形到 AI 推理,WebGPU 正在重新定义浏览器的能力边界。
对于前端开发者来说,现在正是学习 WebGPU 的好时机。API 已经稳定,工具链日趋成熟,生态项目蓬勃发展。即使你暂时不会用到它,理解 GPU 编程的思维方式也是有价值的——并行计算正在成为现代前端工程师的必备技能之一。
下一步,你可以尝试用 WebGPU 实现一个简单的粒子系统,或者跑一个 ONNX 模型的推理。亲身感受 GPU 算力在浏览器中释放的感觉——那种不需要任何后端服务器、数百 TFLOPS 算力触手可及的体验,确实是 Web 开发的下一个篇章。