WebGPU 核心原理与面试
WebGPU 是浏览器下一代图形/计算 API,对应原生端的 Vulkan/Metal/D3D12。它解决的不是「画得更漂亮」,而是「算得更快、并行更彻底」。本文用代码示例带你掌握它的核心概念。
为什么要出 WebGPU?
| 维度 | WebGL 1.0/2.0 | WebGPU |
|---|---|---|
| 底层映射 | OpenGL ES(90 年代设计) | Vulkan / Metal / D3D12(现代设计) |
| API 风格 | 全局状态机(隐式上下文) | 显式对象(Immutable Pipeline,行为可预测) |
| CPU 性能 | Draw Call 状态切换开销大 | 命令缓冲(Command Buffer)预录制 |
| 多线程 | 主线程独占 | 渲染线程、计算任务可并行 |
| 通用计算 | 需「伪装」成纹理(GPGPU) | 原生 Compute Shader + Shared Memory |
| 着色语言 | GLSL | WGSL(类 Rust,强类型) |
一句话总结:WebGL 是「牵着 GPU 走」,WebGPU 是「跟 GPU 下达明确指令清单」。
核心架构:四大对象
要记住一句话:WebGPU 的所有操作都是围绕这四个对象展开的 —— Adapter、Device、Queue、Canvas Context。
// 1. 拿到 GPU 适配器(对应一块物理 GPU)
const adapter = await navigator.gpu.requestAdapter();
// 2. 拿到逻辑设备(逻辑 GPU,后续所有资源都从这创建)
const device = await adapter.requestDevice();
// 3. 拿到命令队列(向 GPU 提交指令的通道)
const queue = device.queue;
// 4. 配置画布(将 WebGPU 与 <canvas> 绑定)
const canvas = document.querySelector("canvas");
const context = canvas.getContext("webgpu");
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
面试要点:adapter 和 device 是异步的。生产环境必须做
navigator.gpu是否存在的降级判断。
3. 第一个三角形:和 WebGL 的对比
WebGL 七步画三角形,WebGPU 把它拆成**预配置(Immutable State)+ 录制命令(Command Encoder)**两步。
3.1 WGSL 着色器源码(取代 GLSL)
// 顶点着色器:@vertex 是入口标识符
@vertex
fn vs_main(@location(0) pos: vec2f) -> @builtin(position) vec4f {
return vec4f(pos, 0.0, 1.0);
}
// 片元着色器:@fragment
@fragment
fn fs_main() -> @location(0) vec4f {
return vec4f(1.0, 0.0, 0.0, 1.0); // 红色
}
WGSL 关键点(面试必问):
@location(0):替代 WebGL 的attribute/varying,显式指定绑定槽位。@builtin(position):等价 GLSL 的gl_Position。vec2f:强类型向量(f=float,i=int,u=uint),不允许隐式转换。
3.2 JS 端:预配置流水线 + 录制命令
// --- 第一步:创建 Shader Module(着色器模块,存编译后的 WGSL)---
const vsModule = device.createShaderModule({ code: vsWGSL });
const fsModule = device.createShaderModule({ code: fsWGSL });
// --- 第二步:创建顶点缓冲区(替代 WebGL 的 VBO)---
const vertices = new Float32Array([0, 0.5, -0.5, -0.5, 0.5, -0.5]);
const vertexBuffer = device.createBuffer({
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexBuffer, 0, vertices);
// --- 第三步:创建 Render Pipeline(不可变状态集)---
// 这是 WebGPU 性能优势的核心:把顶点格式、Shader、混合方式等全部预编译成不可变对象
const pipeline = device.createRenderPipeline({
layout: "auto",
vertex: {
module: vsModule,
entryPoint: "vs_main",
buffers: [
{
arrayStride: 8, // 每个顶点 2 个 float = 8 字节
attributes: [{ shaderLocation: 0, format: "float32x2", offset: 0 }],
},
],
},
fragment: {
module: fsModule,
entryPoint: "fs_main",
targets: [{ format }], // 输出到画布格式
},
primitive: { topology: "triangle-list" },
});
// --- 第四步:录制命令(Command Encoder)---
const commandEncoder = device.createCommandEncoder();
const textureView = context.getCurrentTexture().createView();
const passEncoder = commandEncoder.beginRenderPass({
colorAttachments: [
{
view: textureView,
clearValue: { r: 0, g: 0, b: 0, a: 1 },
loadOp: "clear", // 渲染前清空
storeOp: "store", // 渲染后保存到纹理
},
],
});
passEncoder.setPipeline(pipeline); // 绑定流水线
passEncoder.setVertexBuffer(0, vertexBuffer); // 绑定顶点
passEncoder.draw(3); // 画 3 个顶点
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);
面试话术:WebGL 每次
drawArrays都要 CPU 检查「当前 program 是什么?顶点指针有没有设?」,WebGPU 在createRenderPipeline阶段就把所有状态冻结了,渲染时直接查表,CPU 开销大幅降低。
4. 计算着色器:WebGPU 真正的杀手锏
面试题:WebGPU 相比 WebGL 在「通用计算」上有什么本质提升?
WebGL 方案(GPGPU):把数据伪装成纹理像素。算完用
gl.readPixels读回 CPU,延迟高、跨线程难。WebGPU 方案(Compute Shader):直接分配 Buffer 喂给 GPU,算完直接给 Vertex Shader 用,全程不离开显存。
4.1 经典场景:百万粒子模拟
// compute.wgsl
struct Particle {
pos: vec2f,
vel: vec2f,
};
@group(0) @binding(0) var<storage, read_write> particles: array<Particle>;
@group(0) @binding(1) var<uniform> params: vec2f; // x: dt, y: time
@compute @workgroup_size(64)
fn cs_main(@builtin(global_invocation_id) gid: vec3u) {
let i = gid.x;
if (i >= arrayLength(&particles)) { return; }
// 每个线程更新一个粒子的位置
particles[i].pos += particles[i].vel * params.x;
// 边界反弹
if (particles[i].pos.x > 1.0 || particles[i].pos.x < -1.0) {
particles[i].vel.x = -particles[i].vel.x;
}
}
WGSL Compute 关键概念:
@compute:标记为计算着色器。@workgroup_size(64):每个线程组(Workgroup)有 64 个线程。线程组在 GPU 上以 SIMT 方式并行执行。@builtin(global_invocation_id):当前线程在全局的唯一 ID。var<storage, read_write>:可被 GPU 直接读写的存储缓冲区(对应 WebGL 的texImage2D伪装)。
4.2 JS 端:绑定资源 + 调度计算
// 创建粒子缓冲区(100 万粒子 = 16MB,GPU 显存直接放得下)
const particles = new Float32Array(1_000_000 * 4); // pos.xy + vel.xy
const particleBuffer = device.createBuffer({
size: particles.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(particleBuffer, 0, particles);
// 创建 Compute Pipeline
const computePipeline = device.createComputePipeline({
layout: "auto",
compute: { module: csModule, entryPoint: "cs_main" },
});
// 创建绑定组(Bind Group),把 Buffer「焊」到 Shader 的 @binding 上
const bindGroup = device.createBindGroup({
layout: computePipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: { buffer: particleBuffer } },
{ binding: 1, resource: { buffer: paramsBuffer } },
],
});
// 录制 Compute Pass(每一帧调用)
const encoder = device.createCommandEncoder();
const computePass = encoder.beginComputePass();
computePass.setPipeline(computePipeline);
computePass.setBindGroup(0, bindGroup);
// 1,000,000 / 64 = 15625 个 workgroup,GPU 自动调度
computePass.dispatchWorkgroups(Math.ceil(1_000_000 / 64));
computePass.end();
device.queue.submit([encoder.finish()]);
关键概念:
@workgroup_size(64)是 GPU 调度的最小并行单位。所有 WebGPU 实现都至少支持workgroup_size(256),知道这个限制能帮你避开一些兼容性坑。
5. 三大高频面试题速答
Q1: WebGPU 的「命令录制」模式是怎么省 CPU 的?
答:WebGL 的 gl.drawArrays() 是一次同步调用,GPU 立即执行,CPU 阻塞等待状态校验完成。
WebGPU 的 commandEncoder.beginRenderPass() 是在 CPU 内存里录指令清单,全部录完后 queue.submit() 才一次性推给 GPU。CPU 不再逐帧校验状态,帧间可以预录多帧(Multi-frame Recording),渲染线程与逻辑线程可分离。
Q2: WGSL 为什么强制使用 vec2f 这种类型后缀?
答:GLSL 在 GPU 驱动里会有「隐式转换」歧义(int 和 float 在某些硬件上行为不同),WGSL 直接在语言层面定死 f/i/u 后缀,避免在不同厂商 GPU 上出现精度或行为差异。这是为「跨厂商一致性」而设计,也是 WGSL 强类型 + 类 Rust 语法的根本原因。
Q3: WebGPU 一定比 WebGL 快吗?
答:不一定。对于简单场景(几百个三角形、几张贴图),WebGPU 的「创建 Pipeline 一次成本」反而可能比 WebGL 慢。WebGPU 的优势在于:
- 高频 Draw Call(数千次以上)的场景
- 需要 GPU 通用计算(粒子、ML 推理、图像处理)
- 多线程渲染(Worker 中录指令)
业务选型建议:常规 3D 展示用 WebGL(Three.js)足够;做编辑器、可视化大屏、AI 推理才考虑 WebGPU。
6. 浏览器兼容与降级策略
async function initGPU() {
if (!navigator.gpu) {
throw new Error("当前浏览器不支持 WebGPU");
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error("找不到 GPU 适配器(可能被禁用或被占用)");
}
// 可选:申请错误监控 scope
const device = await adapter.requestDevice();
device.lost.then((info) => {
console.error("GPU 设备丢失:", info.reason, info.message);
});
return device;
}
// 降级到 WebGL
async function initRenderer() {
try {
return await initGPU();
} catch (e) {
console.warn("WebGPU 不可用,降级到 WebGL:", e.message);
// ... 走 Three.js WebGLRenderer 路径
}
}