为什么需要 WebGPU
WebGL 自 2011 年发布以来,为浏览器带来了近十年的 3D 图形能力。然而,WebGL 的设计深受 OpenGL ES 2.0/3.0 的束缚——全局状态机、隐式驱动管理、单线程命令提交,这些特性在移动设备上尚可接受,但在追求极致性能的桌面端和现代 GPU 上已显陈旧。WebGPU 的诞生正是为了解决这些根本性问题。
WebGPU 并非 WebGL 的简单升级,而是一次彻底的架构革新。它吸取了 Vulkan、Metal 和 DirectX 12 三大现代图形 API 的设计理念,将显式控制、低开销驱动和多线程友好带入浏览器环境。与 WebGL 的隐式状态机不同,WebGPU 采用基于对象的显式管线:渲染管线、绑定组、缓冲区和纹理都是独立的对象,状态变化通过对象替换而非全局状态翻转来实现。
在性能层面,WebGPU 的命令缓冲(Command Buffer)机制允许开发者在 JavaScript 主线程上批量录制 GPU 命令,然后一次性提交执行。这种方式不仅减少了 JavaScript 与 GPU 之间的往返开销,还为未来的多线程命令录制(Worker 线程生成命令缓冲)预留了架构空间。实测显示,在相同场景下,WebGPU 的 CPU 开销可比 WebGL 降低 30%~70%,特别是在大量绘制调用(Draw Call)的场景中优势尤为明显。
此外,WebGPU 首创性地将通用 GPU 计算(GPGPU)作为一等公民引入浏览器。Compute Shader 的完整支持使得浏览器端可以进行矩阵运算、图像处理、物理模拟、机器学习推理等计算密集型任务,而无需借助 WebGL 的片段着色器 hack。这为浏览器端 AI 推理、视频编解码加速和科学可视化开辟了新的可能性。
浏览器支持与环境检测
截至 2026 年,WebGPU 已在所有主流浏览器中落地:Chrome 113+、Firefox 118+、Safari 17+ 均提供完整的 WebGPU 支持。Edge 浏览器随 Chromium 内核同步支持。检测浏览器是否支持 WebGPU 非常简单:
if (!navigator.gpu) {
console.error('当前浏览器不支持 WebGPU,请使用最新版 Chrome/Firefox/Safari');
throw new Error('WebGPU not supported');
}
WebGPU API 完全通过 JavaScript 暴露,无需额外插件或扩展。navigator.gpu 是全局入口对象,提供 requestAdapter() 方法用于枚举可用的 GPU 适配器。适配器(GPUAdapter)代表一个物理 GPU 设备,现代笔记本电脑通常有两个适配器:集成核显(power-saving)和独立显卡(high-performance)。
const adapter = await navigator.gpu.requestAdapter({
powerPreference: 'high-performance'
});
if (!adapter) {
throw new Error('无法获取 GPU 适配器');
}
获取适配器后,通过 adapter.requestDevice() 创建一个逻辑设备(GPUDevice)。设备是 WebGPU 编程的核心句柄,所有的缓冲区、纹理、管线和命令都通过设备对象创建:
const device = await adapter.requestDevice({
requiredFeatures: [],
requiredLimits: {}
});
requiredFeatures 可选请求特定硬件特性(如着色器原子操作、纹理压缩格式),requiredLimits 可请求超出默认限制的资源配额(如最大纹理尺寸、绑定组数量)。对于大多数入门场景,使用默认配置即可。
第一个 WebGPU 三角形
每个图形 API 的学习都从绘制一个彩色三角形开始。以下代码完整展示了 WebGPU 从零到三角形的全部步骤,包括着色器代码、渲染管线配置和命令提交。
首先定义 WGSL(WebGPU Shading Language)着色器。WGSL 是 WebGPU 的原生着色器语言,语法类似 Rust 与 GLSL 的混合体:
// 顶点着色器
@vertex
fn vs_main(@location(0) position: vec2<f32>,
@location(1) color: vec3<f32>) -> VertexOutput {
var output: VertexOutput;
output.position = vec4<f32>(position, 0.0, 1.0);
output.color = color;
return output;
}
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) color: vec3<f32>,
}
// 片段着色器
@fragment
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(input.color, 1.0);
}
WGSL 使用显式的阶段标记 @vertex 和 @fragment 区分着色器类型。@location(N) 用于指定顶点属性的插槽索引,@builtin(position) 是裁剪空间位置的专用语义。这种显式标记避免了 GLSL 中 varying 变量命名一致的隐式约定。
接下来创建渲染管线:
const shaderModule = device.createShaderModule({
code: wgslCode
});
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module: shaderModule,
entryPoint: 'vs_main',
buffers: [{
arrayStride: 5 * 4, // 5 floats * 4 bytes
attributes: [
{ shaderLocation: 0, offset: 0, format: 'float32x2' },
{ shaderLocation: 1, offset: 2 * 4, format: 'float32x3' }
]
}]
},
fragment: {
module: shaderModule,
entryPoint: 'fs_main',
targets: [{ format: navigator.gpu.getPreferredCanvasFormat() }]
},
primitive: { topology: 'triangle-list' }
});
WebGPU 的渲染管线是高度结构化的对象。顶点阶段定义了顶点着色器入口、顶点缓冲区布局和属性格式;片段阶段定义了片段着色器入口和输出目标格式;图元阶段指定了拓扑类型(三角形列表、带状、线框等)。值得注意的是,WebGPU 采用 “管线即状态” 的设计:一旦管线创建完成,其配置就是不可变的,运行时切换效果需要创建多个管线对象。
顶点数据通过 GPUBuffer 上传:
const vertices = new Float32Array([
// x, y, r, g, b
0.0, 0.5, 1.0, 0.0, 0.0, // 顶点 1:顶部,红色
-0.5, -0.5, 0.0, 1.0, 0.0, // 顶点 2:左下,绿色
0.5, -0.5, 0.0, 0.0, 1.0 // 顶点 3:右下,蓝色
]);
const vertexBuffer = device.createBuffer({
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
mappedAtCreation: true
});
new Float32Array(vertexBuffer.getMappedRange()).set(vertices);
vertexBuffer.unmap();
mappedAtCreation: true 优化了初始数据上传:缓冲区在创建时即映射到 CPU 可写地址空间,数据写入后通过 unmap() 解除映射,GPU 即可访问。日常使用 device.queue.writeBuffer() 进行动态更新。
最后是渲染循环的核心——命令编码与提交:
function frame() {
const commandEncoder = device.createCommandEncoder();
const renderPass = commandEncoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
loadOp: 'clear',
clearValue: { r: 0.1, g: 0.1, b: 0.1, a: 1.0 },
storeOp: 'store'
}]
});
renderPass.setPipeline(pipeline);
renderPass.setVertexBuffer(0, vertexBuffer);
renderPass.draw(3);
renderPass.end();
device.queue.submit([commandEncoder.finish()]);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
WebGPU 的命令模型是显式的:先创建命令编码器,然后在其上录制渲染通道,设置管线和顶点缓冲,最后调用 draw() 发起绘制。loadOp: 'clear' 表示每帧开始前清空颜色附件,storeOp: 'store' 表示渲染结果写回纹理。命令录制完成后,通过 finish() 编译为可提交的命令缓冲,最终通过设备的队列(Queue)异步提交给 GPU 执行。
Uniform 缓冲与变换矩阵
静态三角形虽能验证管线配置,但真正的 3D 图形需要动态变换。WebGPU 通过 Uniform 绑定组(Bind Group)向着色器传递变换矩阵、光照参数等每帧变化的数据。
WGSL 中声明 uniform 块的方法:
struct Uniforms {
mvp: mat4x4<f32>,
time: f32,
};
@group(0) @binding(0)
var<uniform> u: Uniforms;
@group(M) 和 @binding(N) 组成二维绑定坐标,与 JavaScript 侧的绑定组布局严格对应。在 JavaScript 中创建 uniform 缓冲区:
const uniformBuffer = device.createBuffer({
size: 64 + 4, // mat4 (64 bytes) + float (4 bytes),需对齐到 16 字节
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{
binding: 0,
resource: { buffer: uniformBuffer }
}]
});
每帧更新 uniform 数据时,先计算 MVP 矩阵:
function updateUniforms(time) {
const model = mat4.rotationY(time);
const view = mat4.lookAt(eye, target, up);
const proj = mat4.perspective(fov, aspect, near, far);
const mvp = mat4.multiply(proj, mat4.multiply(view, model));
device.queue.writeBuffer(uniformBuffer, 0, mvp);
device.queue.writeBuffer(uniformBuffer, 64, new Float32Array([time]));
renderPass.setBindGroup(0, bindGroup);
}
WebGPU 的 uniform 缓冲布局遵循 WGSL 的内存布局规则:标量和 vecN 对齐到 4N 字节,mat4x4 对齐到 16 字节,结构体整体对齐到最大成员对齐值。这个布局与 WGSL 的 @align() 和 @size() 属性一致,避免了 GLSL 中 layout (std140) 和 (std430) 的混淆。
纹理加载与采样
2D 贴图是 3D 渲染的核心要素。WebGPU 的纹理 API 比 WebGL 更灵活,支持多层级 mipmap、多种采样模式和丰富的纹理格式。
加载图片并创建纹理:
const img = new Image();
img.src = 'texture.png';
await img.decode();
const texture = device.createTexture({
size: [img.width, img.height],
format: 'rgba8unorm',
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT,
mipLevelCount: 1
});
device.queue.copyExternalImageToTexture(
{ source: img },
{ texture: texture },
[img.width, img.height]
);
创建采样器以控制纹理过滤和寻址模式:
const sampler = device.createSampler({
magFilter: 'linear',
minFilter: 'linear',
mipmapFilter: 'linear',
addressModeU: 'repeat',
addressModeV: 'repeat'
});
在绑定组中绑定纹理和采样器:
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: sampler },
{ binding: 1, resource: texture.createView() },
{ binding: 2, resource: { buffer: uniformBuffer } }
]
});
WGSL 着色器中通过 texture_2d 和 sampler 类型采样:
@group(0) @binding(0) var mySampler: sampler;
@group(0) @binding(1) var myTexture: texture_2d<f32>;
@fragment
fn fs_main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
return textureSample(myTexture, myTexture, uv);
}
textureSample() 函数在 WGSL 中执行硬件双线性(或三线性 mipmap)纹理采样,支持各向异性过滤。WebGPU 的纹理子资源视图系统(Texture View)允许将同一纹理的不同层级、切片或维度格式作为独立绑定资源,这比 WebGL 的单一纹理对象更具表达力。
Compute Shader 与 GPU 通用计算
WebGPU 最具革命性的特性是将 Compute Shader 作为原生功能引入浏览器。Compute Shader 不经过渲染管线,直接在 GPU 的通用计算单元上执行任意并行计算。
以下是一个并行向量加法的 Compute Shader:
@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> c: array<f32>;
@compute @workgroup_size(256)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
let idx = id.x;
if (idx < arrayLength(&a)) {
c[idx] = a[idx] + b[idx];
}
}
@workgroup_size(256) 定义了每个工作组的线程数。NVIDIA GPU 的 Warp 大小为 32,AMD 的 Wavefront 为 64,选择 256(32 和 64 的公倍数)确保线程充分利用。var<storage, read_write> 声明了可读写的存储缓冲区,这是 Compute Shader 传递大规模数据的主要方式。
JavaScript 侧的执行流程:
const computePipeline = device.createComputePipeline({
layout: 'auto',
compute: {
module: shaderModule,
entryPoint: 'main'
}
});
const commandEncoder = device.createCommandEncoder();
const computePass = commandEncoder.beginComputePass();
computePass.setPipeline(computePipeline);
computePass.setBindGroup(0, bindGroup);
computePass.dispatchWorkgroups(Math.ceil(dataLength / 256));
computePass.end();
device.queue.submit([commandEncoder.finish()]);
Compute Shader 的应用场景远超向量加法:图像卷积(高斯模糊、边缘检测)、矩阵乘法(深度学习推理)、物理模拟(粒子系统、流体动力学)、音频处理(FFT 频谱分析)都可以高效实现。以矩阵乘法为例,利用共享内存(var<workgroup>)和分块技术,WebGPU 的 Compute Shader 可以达到理论峰值性能的 80% 以上。
深度测试与多重采样
完整的 3D 渲染需要深度缓冲(Depth Buffer)正确处理遮挡关系。WebGPU 创建深度纹理并在渲染通道中配置:
const depthTexture = device.createTexture({
size: [canvas.width, canvas.height],
format: 'depth24plus',
usage: GPUTextureUsage.RENDER_ATTACHMENT
});
const renderPass = commandEncoder.beginRenderPass({
colorAttachments: [...],
depthStencilAttachment: {
view: depthTexture.createView(),
depthClearValue: 1.0,
depthLoadOp: 'clear',
depthStoreOp: 'store'
}
});
渲染管线的深度状态配置为:
const pipeline = device.createRenderPipeline({
// ...
depthStencil: {
depthWriteEnabled: true,
depthCompare: 'less',
format: 'depth24plus'
}
});
多重采样抗锯齿(MSAA)在 WebGPU 中通过设置 sampleCount: 4 并在渲染管线与纹理中启用多重采样来实现。相比后处理抗锯齿(FXAA、TAA),MSAA 提供更清晰的边缘质量且计算开销可控。
性能优化与最佳实践
WebGPU 的显式控制模型赋予了开发者充分的优化空间,但也要求遵循最佳实践:
最小化命令缓冲提交次数。每次 device.queue.submit() 都会触发 GPU 同步和命令解析开销。理想情况下,一帧内的所有渲染和计算命令应录制到单个命令缓冲中一次性提交。
复用渲染管线对象。管线创建是昂贵的 CPU 操作,涉及驱动层级的着色器编译和状态验证。应将管线对象缓存并在多帧间复用,避免运行时创建。
批量绑定更新。setBindGroup() 的开销虽低于 WebGL 的 uniform 逐个设置,但频繁切换绑定组仍会降低性能。将使用相同材质和纹理的网格排序后批量绘制。
合理使用缓冲区映射。mapAsync() 是 CPU 与 GPU 之间的异步同步点,过度使用会造成管线停顿。对于每帧更新的 uniform 数据,使用 writeBuffer() 而非映射写入。
Compute Shader 的屏障同步。当 Compute Shader 的后续读取依赖前一次写入时(如 ping-pong 缓冲),使用 commandEncoder.pipelineBarrier() 确保内存可见性。WebGPU 的显式屏障模型避免了隐式同步的猜测开销。
调试与工具生态
现代浏览器的开发者工具已深度集成 WebGPU 支持。Chrome DevTools 的 “Performance” 面板可捕获 GPU 时间线,分析每帧的 CPU 录制时间和 GPU 执行时间。“Memory” 面板可追踪 GPU 内存占用,识别纹理和缓冲区的泄漏。
Chrome 的 chrome://gpu 页面详细列出了当前 GPU 的设备信息、驱动版本和 WebGPU 特性支持列表。若遇到渲染异常,首先检查 adapter.info 返回的供应商和架构信息是否匹配预期硬件。
着色器调试方面,WebGPU 的 createShaderModule() 支持 compilationInfo 查询,可获取精确的编译警告和错误位置(行列号)以及错误片段。这比 WebGL 的 getShaderInfoLog() 提供的纯文本日志要精确得多。
WebGPU vs WebGL 选型指南
WebGPU 并非在所有场景下都优于 WebGL。对于简单的 2D 图形、数据可视化图表(D3.js 风格的 SVG/Canvas 替代)或现有大量 WebGL 代码的项目,迁移成本可能超过收益。WebGL 的成熟度、工具生态和学习资源仍然具有优势。
WebGPU 最适合以下场景:高性能 3D 渲染(游戏、CAD、数字孪生)、大规模数据可视化(百万级点渲染)、GPU 通用计算(浏览器端 AI 推理、图像处理)、以及需要现代图形特性的新项目的开发。随着 Safari 和 Firefox 的完整支持,WebGPU 已具备跨生产环境部署的条件。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。