WebGPU 是继 WebGL 之后的新一代浏览器图形与计算 API,它直接映射到现代 GPU 的底层能力(Vulkan、Metal、Direct3D 12),提供了更细粒度的资源管理、计算着色器(Compute Shader)原生支持以及更可预测的性能表现。与 WebGL 那套基于 OpenGL ES 1.0/2.0 的历史包袱不同,WebGPU 从设计之初就考虑到了现代 GPU 架构,使得开发者可以在浏览器中编写真正的高性能并行计算程序。

本文将从 WebGPU 的架构设计讲起,逐步带你完成渲染管线和计算管线的实战编码,覆盖从设备初始化、缓冲区管理、着色器编写(WGSL)到性能优化的完整链路。无论你是从 WebGL 迁移过来,还是第一次接触 GPU 编程,都可以通过本文快速上手 WebGPU 开发。
一、WebGPU 与 WebGL 的核心差异
要理解 WebGPU 的价值,首先需要明白它在架构层面与 WebGL 的根本不同。WebGL 是 OpenGL ES 的浏览器封装,继承了大量历史遗留概念,比如全局状态机、隐式同步等。而 WebGPU 采用了显式的、面向对象的 API 设计,所有资源(缓冲区、纹理、采样器)都是独立的对象,状态不再全局共享。
| 特性 | WebGL 2.0 | WebGPU |
|---|---|---|
| 底层映射 | OpenGL ES 3.0 | Vulkan / Metal / D3D12 |
| 着色器语言 | GLSL | WGSL (WebGPU Shading Language) |
| 状态管理 | 全局状态机 | 显式对象 + 管线对象 |
| 计算着色器 | 不支持(需 Transform Feedback 变通) | 原生支持 |
| 命令录制 | 即时模式(立即执行) | 命令缓冲区(延迟执行) |
| 错误处理 | 异步回调 + 黑盒 | 结构化验证 + 详细错误信息 |
| 多线程 | 主线程限制 | Worker 中可创建命令缓冲区 |
其中最重要的变化是命令缓冲区(Command Buffer)机制。在 WebGL 中,每次绘制调用都是立即执行的,GPU 驱动需要不断同步状态。而 WebGPU 允许你将一系列渲染或计算命令录制到缓冲区中,然后一次性提交执行,大幅减少了 CPU-GPU 之间的通信开销。
二、设备初始化与适配器选择
WebGPU 的入口是
1 | navigator.gpu |
对象。使用前需要请求一个适配器(Adapter,代表物理 GPU)和设备(Device,代表逻辑 GPU 上下文)。这个过程是异步的,因为浏览器需要确认用户硬件是否支持 WebGPU,并在必要时进行降级处理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 async function initWebGPU(canvas) {
// 1. 检查浏览器是否支持 WebGPU
if (!navigator.gpu) {
throw new Error('当前浏览器不支持 WebGPU,请使用 Chrome 113+ 或 Safari 18+');
}
// 2. 请求适配器(物理 GPU)
const adapter = await navigator.gpu.requestAdapter({
powerPreference: 'high-performance', // 或 'low-power'
forceFallbackAdapter: false,
});
if (!adapter) {
throw new Error('找不到合适的 GPU 适配器');
}
// 3. 请求设备(逻辑 GPU 上下文)
const device = await adapter.requestDevice({
requiredFeatures: ['timestamp-query', 'indirect-first-instance'],
requiredLimits: {
maxStorageBufferBindingSize: 128 * 1024 * 1024, // 128MB
maxComputeWorkgroupsPerDimension: 65535,
},
});
// 4. 配置画布上下文
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({
device,
format,
alphaMode: 'premultiplied',
});
return { device, context, format };
}
注意
1 | requestDevice |
中的
1 | requiredFeatures |
和
1 | requiredLimits |
。这是 WebGPU 的一个重要设计:你可以在请求设备时声明所需的特性和限制,如果硬件不满足,浏览器会直接拒绝创建设备。这比 WebGL 中运行时才发现功能不可用要好得多。如果你希望更宽松地降级,可以改用
1 | optionalFeatures |
。
适配器信息查询
你可以通过
1 | adapter.info |
获取 GPU 的详细信息,用于日志记录和性能调优决策:
1
2
3
4
5 const info = adapter.info;
console.log(`GPU 厂商: ${info.vendor}`);
console.log(`GPU 架构: ${info.architecture}`);
console.log(`设备描述: ${info.description}`);
// 例如:vendor='nvidia', architecture='rtx-4070'
三、WGSL 着色器语言基础
WebGPU 引入了全新的着色器语言 WGSL(WebGPU Shading Language)。它的语法更接近 Rust 和 TypeScript,类型系统更严格,避免了 GLSL 中常见的隐式转换陷阱。WGSL 代码可以通过
1 | device.createShaderModule |
编译为 GPU 可执行的管线。
下面是一个顶点+片段着色器的基本示例,实现了经典的彩色三角形渲染:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 const shaderCode = `
// 顶点着色器输入
struct VertexInput {
@location(0) position: vec2f,
@location(1) color: vec3f,
};
// 顶点着色器输出(传递给片段着色器)
struct VertexOutput {
@builtin(position) clip_position: vec4f,
@location(0) color: vec3f,
};
@vertex
fn vs_main(input: VertexInput) -> VertexOutput {
var output: VertexOutput;
output.clip_position = vec4f(input.position, 0.0, 1.0);
output.color = input.color;
return output;
}
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4f {
return vec4f(input.color, 1.0);
}
`;
const shaderModule = device.createShaderModule({
code: shaderCode,
});
关键语法要点:
-
1@vertex
/
1@fragment/
1@compute是入口点标注,每个着色器模块可以有多个入口点
-
1@location(N)
定义了顶点属性的插槽编号,需要与缓冲区布局对应
-
1@builtin(position)
是内置变量,表示裁剪空间坐标
- 类型后缀如
1vec2f
、
1vec4f表示精度(f = f32),也有
1vec2u、
1vec3i等变体
四、渲染管线实战:从三角形到完整渲染流程

现在我们将上面的组件组装成一条完整的渲染管线。WebGPU 的渲染管线是一个不可变的管线对象(RenderPipeline),它预先绑定了着色器、布局格式和混合状态,这样 GPU 在执行时不需要反复切换状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48 function createRenderPipeline(device, format, shaderModule) {
// 顶点缓冲区布局
const vertexBufferLayout = {
arrayStride: 20, // 每个顶点 20 字节:position(8) + color(12)
attributes: [
{
format: 'float32x2', // vec2 position
offset: 0,
shaderLocation: 0,
},
{
format: 'float32x3', // vec3 color
offset: 8,
shaderLocation: 1,
},
],
};
const pipeline = device.createRenderPipeline({
label: 'Triangle Pipeline',
layout: 'auto', // 自动推导绑定组布局
vertex: {
module: shaderModule,
entryPoint: 'vs_main',
buffers: [vertexBufferLayout],
},
fragment: {
module: shaderModule,
entryPoint: 'fs_main',
targets: [
{
format: format,
blend: {
color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha' },
alpha: { srcFactor: 'one', dstFactor: 'zero' },
},
},
],
},
primitive: {
topology: 'triangle-list',
cullMode: 'back',
frontFace: 'ccw',
},
});
return pipeline;
}
接下来是顶点数据创建和渲染循环。注意 WebGPU 中缓冲区的创建是显式的,你需要指定 usage 标志来告诉 GPU 这个缓冲区将如何被使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 // 三角形顶点数据:position(xy) + color(rgb)
const vertices = new Float32Array([
// 顶部 - 红色
0.0, 0.5, 1.0, 0.0, 0.0,
// 左下 - 绿色
-0.5, -0.5, 0.0, 1.0, 0.0,
// 右下 - 蓝色
0.5, -0.5, 0.0, 0.0, 1.0,
]);
const vertexBuffer = device.createBuffer({
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
// 将数据写入缓冲区(通过临时的 staging buffer 或直接写入)
device.queue.writeBuffer(vertexBuffer, 0, vertices);
// 渲染循环
function render() {
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: { r: 0.1, g: 0.1, b: 0.1, a: 1.0 },
loadOp: 'clear',
storeOp: 'store',
}],
});
pass.setPipeline(pipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.draw(3); // 绘制 3 个顶点
pass.end();
device.queue.submit([encoder.finish()]);
requestAnimationFrame(render);
}
这段代码展示了 WebGPU 渲染的核心模式:命令录制 → 提交执行。所有 GPU 操作都先录制到
1 | CommandEncoder |
中,最后通过
1 | queue.submit |
一次性提交。这种设计让浏览器和 GPU 驱动有更大的优化空间,比如可以对命令进行重排和合并。
五、计算管线实战:GPU 并行计算入门
WebGPU 最令人兴奋的特性之一是原生支持计算着色器。这意味着你可以在浏览器中利用 GPU 进行大规模并行计算,应用场景包括物理模拟、图像处理、机器学习推理等。这是 WebGL 时代难以高效实现的能力。
下面我们实现一个经典的 GPU 并行计算示例:将一个大数组中的每个元素乘以 2。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65 const computeShader = `
@group(0) @binding(0) var<storage, read_write> data: array<f32>;
@compute
@workgroup_size(64)
fn main(@builtin(global_invocation_id) id: vec3u) {
if (id.x >= arrayLength(&data)) {
return;
}
data[id.x] = data[id.x] * 2.0;
}
`;
async function gpuComputeExample(device) {
const dataSize = 1024 * 1024; // 100万个浮点数
const inputData = new Float32Array(dataSize);
for (let i = 0; i < dataSize; i++) {
inputData[i] = Math.random() * 100;
}
// 创建 storage buffer(GPU 可读写)
const storageBuffer = device.createBuffer({
size: inputData.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(storageBuffer, 0, inputData);
// 创建结果读取缓冲区
const resultBuffer = device.createBuffer({
size: inputData.byteLength,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
const shaderModule = device.createShaderModule({ code: computeShader });
const computePipeline = device.createComputePipeline({
layout: 'auto',
compute: { module: shaderModule, entryPoint: 'main' },
});
const bindGroup = device.createBindGroup({
layout: computePipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: storageBuffer } }],
});
// 计算并录制命令
const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(computePipeline);
pass.setBindGroup(0, bindGroup);
const workgroupCount = Math.ceil(dataSize / 64);
pass.dispatchWorkgroups(workgroupCount);
pass.end();
// 将结果拷贝到可读缓冲区
encoder.copyBufferToBuffer(storageBuffer, 0, resultBuffer, 0, inputData.byteLength);
device.queue.submit([encoder.finish()]);
// 读取结果
await resultBuffer.mapAsync(GPUMapMode.READ);
const result = new Float32Array(resultBuffer.getMappedRange());
console.log('GPU 计算结果前5项:', Array.from(result.slice(0, 5)));
resultBuffer.unmap();
}
这里有几个关键概念需要理解:
- workgroup_size(64):每个工作组包含 64 个线程,这是 GPU 硬件调度的基本单位
- global_invocation_id:全局线程 ID,用于索引数据数组
- dispatchWorkgroups(N):启动 N 个工作组,总线程数 = N × workgroup_size
- storage buffer:GPU 可读写的高性能缓冲区,适合大规模数据
六、缓冲区与数据传输策略
WebGPU 中的缓冲区管理是性能优化的关键。不同用途的缓冲区需要不同的 usage 标志组合,选错了会导致性能下降甚至运行时错误。
| 缓冲区类型 | 常用 Usage | 适用场景 |
|---|---|---|
| Vertex Buffer | VERTEX | COPY_DST | 顶点数据 |
| Index Buffer | INDEX | COPY_DST | 索引数据 |
| Uniform Buffer | UNIFORM | COPY_DST | 着色器常量(变换矩阵等) |
| Storage Buffer | STORAGE | COPY_SRC | COPY_DST | 计算着色器数据 |
| Staging Buffer | MAP_READ | COPY_SRC | CPU 读取 GPU 数据 |
| Map Write Buffer | MAP_WRITE | COPY_SRC | CPU 写入后拷贝到 GPU |
一个常见的性能陷阱是频繁地 map/unmap 缓冲区。正确的做法是使用
1 | device.queue.writeBuffer |
来上传数据(内部会自动优化),只在需要读取 GPU 计算结果时才使用 staging buffer。下面是一个高效更新 Uniform Buffer 的模式:
1
2
3
4
5
6
7
8
9
10 // 每帧更新变换矩阵
const uniformBuffer = device.createBuffer({
size: 64, // 4x4 矩阵 = 64 字节
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
function updateTransform(matrix) {
// queue.writeBuffer 会自动处理对齐和时序
device.queue.writeBuffer(uniformBuffer, 0, matrix);
}
七、纹理与采样器
WebGPU 的纹理处理比 WebGL 更加灵活。你可以创建 2D 纹理、立方体纹理、3D 纹理等,并通过采样器对象控制过滤方式。纹理数据的上传支持多种格式,包括压缩纹理格式(如 BC7、ETC2),这在移动端可以显著减少内存占用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 async function createTextureFromImage(device, url) {
const response = await fetch(url);
const blob = await response.blob();
const imageBitmap = await createImageBitmap(blob);
const texture = device.createTexture({
size: [imageBitmap.width, imageBitmap.height],
format: 'rgba8unorm',
usage: GPUTextureUsage.TEXTURE_BINDING |
GPUTextureUsage.COPY_DST |
GPUTextureUsage.RENDER_ATTACHMENT,
});
device.queue.copyExternalImageToTexture(
{ source: imageBitmap },
{ texture, premultipliedAlpha: true },
[imageBitmap.width, imageBitmap.height]
);
const sampler = device.createSampler({
magFilter: 'linear',
minFilter: 'linear',
mipmapFilter: 'linear',
addressModeU: 'repeat',
addressModeV: 'repeat',
});
return { texture, sampler };
}
八、性能优化最佳实践
在掌握了 WebGPU 的基本用法后,性能优化是进阶的关键。以下是在实际项目中最有效的优化策略:
1. 管线对象复用
渲染管线对象的创建开销很大,应该在初始化时创建一次并全程复用。避免在渲染循环中创建管线,这是新手最常犯的错误。
2. 命令缓冲区批处理
尽量将多个绘制调用合并到一个命令缓冲区中,减少 submit 的调用次数。如果你的场景有数百个物体,可以录制一个大命令缓冲区一次提交。
3. 异步管线创建
1
2
3
4
5
6
7
8
9
10
11
12 // WebGPU 支持异步创建管线,避免阻塞主线程
const [pipeline, computePipeline] = await Promise.all([
device.createRenderPipelineAsync({
layout: 'auto',
vertex: { module: shaderModule, entryPoint: 'vs_main', buffers: [layout] },
fragment: { module: shaderModule, entryPoint: 'fs_main', targets: [{ format }] },
}),
device.createComputePipelineAsync({
layout: 'auto',
compute: { module: shaderModule, entryPoint: 'compute_main' },
}),
]);
4. 多线程命令录制
WebGPU 允许在 Web Worker 中创建和填充命令缓冲区,然后传回主线程提交。这对于复杂场景的渲染非常有用,可以将命令录制的工作从主线程卸载。
5. 间接绘制(Indirect Drawing)
使用
1 | drawIndirect |
和
1 | drawIndexedIndirect |
可以让 GPU 自己决定绘制参数,实现 GPU 驱动渲染(GPU-driven rendering),减少 CPU 到 GPU 的数据传输。
1
2
3
4
5
6
7
8
9 // 间接绘制:绘制参数存储在缓冲区中
const indirectBuffer = device.createBuffer({
size: 16, // vertexCount, instanceCount, firstVertex, firstInstance
usage: GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(indirectBuffer, 0, new Uint32Array([3, 1, 0, 0]));
// 在渲染通道中
pass.drawIndirect(indirectBuffer, 0);
九、浏览器兼容性与降级策略
截至目前(2026年),WebGPU 已在 Chrome 113+、Edge 113+ 和 Safari 18+ 中得到支持,Firefox 仍在逐步推进中。在实际项目中,建议做好特性检测和降级处理:
1
2
3
4
5
6
7
8
9
10
11 async function initGraphics(canvas) {
if (navigator.gpu) {
try {
return await initWebGPU(canvas);
} catch (e) {
console.warn('WebGPU 初始化失败,降级到 WebGL:', e);
}
}
// 降级到 WebGL2 或 WebGL
return initWebGL(canvas);
}
对于生产环境,可以考虑使用
1 | @webgpu/types |
包来获得完整的 TypeScript 类型声明,推荐在所有 WebGPU 项目中使用以获得更好的开发体验和类型安全。
十、总结与展望
WebGPU 代表了浏览器图形编程的未来。它不仅在渲染能力上全面超越了 WebGL,更重要的是引入了计算着色器这一革命性能力,使得在浏览器中运行 GPU 加速的物理模拟、图像处理甚至机器学习推理成为可能。
从架构设计来看,WebGPU 的显式 API 虽然在学习曲线上比 WebGL 更陡峭,但换来的是更可预测的性能、更强大的功能和更好的错误处理。对于有 WebGL 经验的开发者,迁移到 WebGPU 最大的思维转变是从”全局状态机”模式转向”显式对象 + 命令录制”模式。
随着 WebGPU 的生态逐渐成熟,我们可以期待更多基于它的框架和工具出现。如果你正在开发对图形性能有较高要求的 Web 应用,现在是开始学习和采用 WebGPU 的最佳时机。
- WebGPU 官方规范:https://www.w3.org/TR/webgpu/
- WGSL 规范:https://www.w3.org/TR/WGSL/
- WebGPU 示例集:https://webgpu.github.io/webgpu-samples/
汤不热吧