Skip to main content

WebGPU 核心原理与面试

WebGPU 是浏览器下一代图形/计算 API,对应原生端的 Vulkan/Metal/D3D12。它解决的不是「画得更漂亮」,而是「算得更快、并行更彻底」。本文用代码示例带你掌握它的核心概念。

为什么要出 WebGPU?

维度WebGL 1.0/2.0WebGPU
底层映射OpenGL ES(90 年代设计)Vulkan / Metal / D3D12(现代设计)
API 风格全局状态机(隐式上下文)显式对象(Immutable Pipeline,行为可预测)
CPU 性能Draw Call 状态切换开销大命令缓冲(Command Buffer)预录制
多线程主线程独占渲染线程、计算任务可并行
通用计算需「伪装」成纹理(GPGPU)原生 Compute Shader + Shared Memory
着色语言GLSLWGSL(类 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 路径
}
}

7. 参考