摘要:WebGL 统治了 Web 图形十余年,WebGPU 则在 2023--2025 年间完成了从实验特性到全平台默认可用的跃迁。本文用「一个三角形」把两条技术路线走通:先讲清两者的定位与图形管线心智模型,再分别给出 WebGL 2 与 WebGPU 的完整可运行示例、uniform 动画、WebGPU 独有的计算着色器,最后给出选型决策矩阵与迁移路径。所有代码均可直接复制到本地 HTML 文件运行。
关键词:WebGL、WebGPU、WGSL、GLSL、计算着色器、渲染管线、Three.js
一、先别急着写代码:三个必须搞清楚的定位问题
1.1 WebGL 是什么
WebGL 是 OpenGL ES 的 JavaScript 绑定 。WebGL 1.0 发布于 2011 年,对应 OpenGL ES 2.0;WebGL 2.0 发布于 2017 年,对应 OpenGL ES 3.0。它的核心特征是全局状态机 :你调用 gl.bindBuffer()、gl.useProgram()、gl.enable() 去修改一个隐藏的全局状态,然后 gl.drawArrays() 按照这套"当前状态"去画。
这个模型在 2011 年是合理的,但它有两个绕不开的问题:
- 驱动负担重:OpenGL 的设计年代早于现代 GPU 架构,浏览器和驱动需要大量猜测与校验才能把状态翻译成 GPU 指令;
- CPU 开销高:每次 draw call 前的状态切换都是 CPU 端的验证工作,场景复杂时 CPU 先于 GPU 成为瓶颈。
1.2 WebGPU 是什么
WebGPU 是 W3C 制定的全新 Web 图形与计算 API,它的设计目标是"高效映射到 2014 年之后的原生 GPU API"------Windows 上走 Direct3D 12,macOS/iOS 上走 Metal,Linux/Android 上走 Vulkan,底层由 Chrome 的 Dawn、Firefox 的 wgpu(Rust)等实现。
它带来了三个结构性变化:
| 特性 | WebGL | WebGPU |
|---|---|---|
| 状态管理 | 全局状态机 | 不可变的管线对象(Pipeline)+ 绑定组(Bind Group) |
| 命令提交 | 每次调用立即提交 | 录制到命令缓冲(Command Buffer),批量提交,可在 Worker 中录制 |
| 通用计算 | 只能通过片元着色器"伪装" | 一等公民的计算着色器(Compute Shader) |
1.3 WebGPU 不是「WebGL 3.0」
这是入门时最重要的澄清,也是最容易踩的认知坑:
- API 层面毫无继承关系。W3C 规范里明确写着 "WebGPU is not related to WebGL"。着色器语言从 GLSL 换成 WGSL,两者不能混用。
- WebGL 不会被移除。规范制定者的公开立场是 WebGL 与 WebGPU 长期共存,WebGL 已进入"功能冻结"状态------不再新增特性,但也不会消失。
- 性能收益有前提 。WebGPU 的优势集中在高 draw call 数量、GPU 计算、实例化渲染等场景;如果你的场景只有几十个 draw call,WebGPU 的接口开销甚至可能略高于 WebGL。
1.4 2026 年的浏览器支持现状
查支持率请认准 caniuse.com/webgpu,以下是截至 2026 年 10 月的情况(数字会持续变化,以 caniuse 为准):
| 浏览器 | 默认启用版本 | 平台覆盖 |
|---|---|---|
| Chrome / Edge | 113+(2023-05) | Windows、macOS、ChromeOS;Android 12+ 自 121 起;Linux 自 144 起逐步放开 |
| Safari | 26+(2025-09) | macOS Tahoe 26、iOS 26、iPadOS 26、visionOS 26 |
| Firefox | 141+(Windows,2025-07)、145/147+(macOS Apple Silicon) | Linux、Intel Mac、Android 尚在 Nightly / 需手动开启 |
| Samsung Internet | 24+ | Android |
全球覆盖率约 85%--87% 。规范本身目前仍是 W3C 候选推荐草案(CRD),最新草案持续更新中------这意味着核心 API 已稳定(2023 年写的代码今天照样能跑),但尚未走完正式推荐流程。
实践结论 :桌面端与主流移动端可以放心把 WebGPU 作为主路径,但必须保留 WebGL 2 回退,尤其要覆盖 Linux 与 Firefox 用户。
二、图形管线的通用心智模型
先把框架立住。无论 WebGL 还是 WebGPU,一次绘制的本质都是同一件事:
CPU 端 GPU 端
───── ─────
准备顶点数据(位置/颜色/法线/UV)
│
▼
上传到显存(Buffer)
│
▼
编写着色器 ─────────────────► 顶点着色器:决定每个顶点落在屏幕哪里
(GLSL / WGSL) │
│ ▼
│ 图元装配 + 光栅化:顶点 → 三角形 → 像素
│ │
│ ▼
│ 片元着色器:决定每个像素是什么颜色
│ │
▼ ▼
配置渲染状态 ──────────────► 深度/模板测试 + 混合 → 写入画布
记住这四件事,剩下的 API 细节都是"怎么把这几件事告诉 GPU":
- 数据怎么进去(Buffer / 顶点布局)
- 着色器怎么描述(GLSL / WGSL)
- 状态怎么组织(WebGL 的全局状态 vs WebGPU 的管线对象)
- 命令怎么发出(立即调用 vs 命令缓冲)
三、环境准备与能力检测
3.1 检测 WebGL 2
js
function initWebGL2(canvas) {
const gl = canvas.getContext('webgl2');
if (!gl) {
console.error('当前环境不支持 WebGL 2');
return null;
}
// 顺手打印一下 GPU 信息,调试时很有用
const info = gl.getExtension('WEBGL_debug_renderer_info');
if (info) {
console.log('GPU:', gl.getParameter(info.UNMASKED_RENDERER_WEBGL));
}
return gl;
}
3.2 检测 WebGPU ------ 必须分两步
这是新手最常见的错误:只判断 navigator.gpu 是否存在。实际上 navigator.gpu 存在 ≠ 真的能拿到 GPU ,驱动被浏览器列入黑名单时 requestAdapter() 会返回 null。
js
async function checkWebGPU() {
if (!('gpu' in navigator)) {
return { ok: false, reason: '浏览器未实现 WebGPU API' };
}
try {
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
return { ok: false, reason: '浏览器支持 API,但拿不到适配器(驱动被黑名单 / 无可用 GPU)' };
}
const device = await adapter.requestDevice();
return { ok: true, device, adapter };
} catch (e) {
return { ok: false, reason: e.message };
}
}
3.3 本地运行方式
两个 API 都要求页面运行在安全上下文 中------http://localhost 或 https://。直接双击打开 .html 文件(file://)在部分浏览器下会失败,建议起个本地服务:
bash
# 任选其一
npx serve .
python3 -m http.server 8000
四、WebGL 2 入门:第一个彩色三角形
把下面代码保存为 webgl-triangle.html 直接运行,会看到一个渐变色的三角形。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebGL 2 三角形</title>
<style>body{margin:0;background:#14161c}canvas{display:block;width:100vw;height:100vh}</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const canvas = document.getElementById('canvas');
const gl = canvas.getContext('webgl2');
if (!gl) throw new Error('当前环境不支持 WebGL 2');
/* ── 1. 适配高分屏 ───────────────────────────── */
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.floor(canvas.clientWidth * dpr);
canvas.height = Math.floor(canvas.clientHeight * dpr);
gl.viewport(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
resize();
/* ── 2. 着色器源码(GLSL ES 3.00)──────────────── */
const vsSource = `#version 300 es
in vec2 a_position; // 顶点位置
in vec3 a_color; // 顶点颜色
out vec3 v_color; // 传给片元着色器
void main() {
v_color = a_color;
gl_Position = vec4(a_position, 0.0, 1.0);
}`;
const fsSource = `#version 300 es
precision highp float;
in vec3 v_color;
out vec4 outColor; // WebGL 2 用 out 变量替代 gl_FragColor
void main() {
outColor = vec4(v_color, 1.0);
}`;
/* ── 3. 编译着色器(务必检查错误!)────────────── */
function compileShader(type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
const log = gl.getShaderInfoLog(shader);
gl.deleteShader(shader);
throw new Error('着色器编译失败:\n' + log);
}
return shader;
}
function createProgram(vsSrc, fsSrc) {
const program = gl.createProgram();
gl.attachShader(program, compileShader(gl.VERTEX_SHADER, vsSrc));
gl.attachShader(program, compileShader(gl.FRAGMENT_SHADER, fsSrc));
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
throw new Error('程序链接失败:\n' + gl.getProgramInfoLog(program));
}
return program;
}
const program = createProgram(vsSource, fsSource);
/* ── 4. 顶点数据(交错存储:x, y, r, g, b)──────── */
const vertices = new Float32Array([
0.0, 0.5, 1.0, 0.2, 0.3, // 顶部,偏红
-0.5, -0.5, 0.2, 1.0, 0.6, // 左下,偏绿
0.5, -0.5, 0.3, 0.5, 1.0, // 右下,偏蓝
]);
/* ── 5. VAO + VBO:记录顶点属性的读取方式 ─────── */
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
const vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const STRIDE = 5 * 4; // 每个顶点 5 个 float = 20 字节
const posLoc = gl.getAttribLocation(program, 'a_position');
const colorLoc = gl.getAttribLocation(program, 'a_color');
gl.enableVertexAttribArray(posLoc);
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, STRIDE, 0); // 从偏移 0 读 2 个 float
gl.enableVertexAttribArray(colorLoc);
gl.vertexAttribPointer(colorLoc, 3, gl.FLOAT, false, STRIDE, 2 * 4); // 从偏移 8 读 3 个 float
/* ── 6. 绘制 ──────────────────────────────────── */
gl.clearColor(0.08, 0.09, 0.11, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.bindVertexArray(vao);
gl.drawArrays(gl.TRIANGLES, 0, 3);
</script>
</body>
</html>
4.1 关键 API 速查
| API | 作用 |
|---|---|
gl.createShader / shaderSource / compileShader |
创建并编译着色器 |
gl.createProgram / attachShader / linkProgram |
链接成 GPU 程序 |
gl.createBuffer / bindBuffer / bufferData |
把数据传进显存 |
gl.createVertexArray / bindVertexArray |
VAO,缓存顶点属性配置(WebGL 2 才有,务必用) |
gl.getAttribLocation / vertexAttribPointer |
告诉 GPU 顶点数据怎么切分给各个 in 变量 |
gl.getUniformLocation / uniformXX |
传 uniform 值 |
gl.drawArrays / drawElements |
发起绘制 |
4.2 WebGL 新手必踩的五个坑
- 忘记检查编译错误 。着色器写错时 WebGL 不会抛异常 ,只会静默画黑屏。一定要
getShaderParameter(..., COMPILE_STATUS)。 - canvas 的 CSS 尺寸 ≠ 画布像素尺寸 。不处理
devicePixelRatio会导致画面模糊,不调gl.viewport()会导致画面错位。 gl_FragColor在 WebGL 2 里已废弃 ,改用out vec4 outColor;。- 忘了
enableVertexAttribArray。属性默认关闭,不启用就读不到数据。 - NDC 坐标系 :屏幕坐标范围是
[-1, 1],Y 轴向上为正(与 DOM 相反),原点在中心。
五、WebGPU 入门:同一个三角形
保存为 webgpu-triangle.html。代码比 WebGL 长一些,但结构清晰得多------你在创建对象,而不是在改状态。
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebGPU 三角形</title>
<style>body{margin:0;background:#14161c}canvas{display:block;width:100vw;height:100vh}</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script type="module">
const canvas = document.getElementById('canvas');
/* ── 1. 适配器 → 设备 ──────────────────────────── */
if (!navigator.gpu) throw new Error('浏览器不支持 WebGPU,请升级到 Chrome 113+ / Safari 26+ / Firefox 141+');
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error('拿不到 GPU 适配器');
const device = await adapter.requestDevice();
// 优雅处理设备丢失(驱动崩溃、显存耗尽、标签页被系统回收)
device.lost.then((info) => {
console.error('WebGPU 设备丢失:', info.message, '| reason:', info.reason);
// 生产环境:提示用户并重建设备,注意重建时要重新 requestAdapter()
});
/* ── 2. 配置画布 ───────────────────────────────── */
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat(); // 通常是 bgra8unorm
context.configure({ device, format, alphaMode: 'opaque' });
/* ── 3. WGSL 着色器 ────────────────────────────── */
const code = `
struct VertexOutput {
@builtin(position) position : vec4f,
@location(0) color : vec3f,
};
@vertex
fn vs(@location(0) pos : vec2f,
@location(1) col : vec3f) -> VertexOutput {
var out : VertexOutput;
out.position = vec4f(pos, 0.0, 1.0);
out.color = col;
return out;
}
@fragment
fn fs(in : VertexOutput) -> @location(0) vec4f {
return vec4f(in.color, 1.0);
}
`;
const module = device.createShaderModule({ code });
/* ── 4. 顶点缓冲 ───────────────────────────────── */
const vertices = new Float32Array([
0.0, 0.5, 1.0, 0.2, 0.3,
-0.5, -0.5, 0.2, 1.0, 0.6,
0.5, -0.5, 0.3, 0.5, 1.0,
]);
const vertexBuffer = device.createBuffer({
size: vertices.byteLength,
usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(vertexBuffer, 0, vertices);
/* ── 5. 渲染管线:把"状态"一次性打包成不可变对象 ── */
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module,
entryPoint: 'vs',
buffers: [{
arrayStride: 5 * 4,
attributes: [
{ shaderLocation: 0, offset: 0, format: 'float32x2' }, // pos
{ shaderLocation: 1, offset: 2 * 4, format: 'float32x3' }, // col
],
}],
},
fragment: {
module,
entryPoint: 'fs',
targets: [{ format }],
},
primitive: { topology: 'triangle-list' },
});
/* ── 6. 录制命令 → 提交 ────────────────────────── */
function frame() {
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(), // 每帧都要重新取!
clearValue: { r: 0.08, g: 0.09, b: 0.11, a: 1.0 },
loadOp: 'clear',
storeOp: 'store',
}],
});
pass.setPipeline(pipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.draw(3);
pass.end();
device.queue.submit([encoder.finish()]);
}
frame();
</script>
</body>
</html>
5.1 WebGPU 的对象模型
navigator.gpu 入口
│ requestAdapter()
▼
GPUAdapter 物理显卡的抽象
│ requestDevice()
▼
GPUDevice ──────────────────────── 所有资源的工厂
├── createBuffer() GPUBuffer 顶点/uniform/存储/回读
├── createTexture() GPUTexture 纹理、深度缓冲、画布
├── createShaderModule() GPUShaderModule(WGSL)
├── createRenderPipeline() GPURenderPipeline 打包好的渲染状态
├── createComputePipeline() GPUComputePipeline
├── createBindGroup() GPUBindGroup 资源与着色器槽位的映射
└── createCommandEncoder() GPUCommandEncoder
│ beginRenderPass() / beginComputePass()
▼
GPUPassEncoder → 录制 draw/dispatch → end()
│ finish()
▼
GPUCommandBuffer → device.queue.submit([...])
5.2 WebGPU 新手必踩的五个坑
- 初始化是异步的 。
requestAdapter()和requestDevice()都返回 Promise,顶层await需要<script type="module">。 - 忘记
context.configure()。不配置画布,getCurrentTexture()直接报错。 getCurrentTexture()每帧都要重新调用。画布尺寸变化或页面合成后纹理会失效,缓存下来会导致画面不更新。- WGSL 里
struct输出不能直接用@builtin(position)之外的 builtin 传给片元 ------要用@location(n)自定义成员(如上例的color)。 - 缓冲区 usage 必须提前声明 。
GPUBufferUsage.VERTEX | COPY_DST少写一个就会在创建时报错,且不可后期补加。
5.3 捕获 GPU 错误
WebGPU 的默认行为是把错误推到控制台,但生产代码应该主动捕获:
js
device.pushErrorScope('validation');
// ... 可能出错的 GPU 调用 ...
const error = await device.popErrorScope();
if (error) console.error('GPU 校验错误:', error.message);
WGSL 编译警告则应监听着色器模块:
js
const info = await module.getCompilationInfo();
for (const m of info.messages) {
console.log(`${m.type} [${m.lineNum}:${m.linePos}] ${m.message}`);
}
六、让三角形转起来:uniform 的两种写法
静态图形没意思,加个时间参数。这是两者差异体现得最直观的地方。
6.1 WebGL:逐个 uniform 设置
js
// 顶点着色器里加:
// uniform float u_angle;
// gl_Position = vec4(a_position.x * cos(u_angle) - a_position.y * sin(u_angle),
// a_position.x * sin(u_angle) + a_position.y * cos(u_angle), 0.0, 1.0);
const angleLoc = gl.getUniformLocation(program, 'u_angle');
function render(time) {
gl.clear(gl.COLOR_BUFFER_BIT);
gl.useProgram(program);
gl.bindVertexArray(vao);
gl.uniform1f(angleLoc, time * 0.001); // 每次 draw 前设一次
gl.drawArrays(gl.TRIANGLES, 0, 3);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
6.2 WebGPU:uniform 缓冲 + 绑定组
WebGPU 没有单独的"设一个 float"这种 API,所有 uniform 都走缓冲:
wgsl
struct Uniforms {
angle : f32,
pad : vec3f, // uniform 缓冲按 16 字节对齐,显式补齐更稳妥
};
@group(0) @binding(0) var<uniform> u : Uniforms;
@vertex
fn vs(@location(0) pos : vec2f, @location(1) col : vec3f) -> VertexOutput {
let s = sin(u.angle);
let c = cos(u.angle);
var out : VertexOutput;
out.position = vec4f(pos.x * c - pos.y * s, pos.x * s + pos.y * c, 0.0, 1.0);
out.color = col;
return out;
}
js
const uniformBuffer = device.createBuffer({
size: 16,
usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});
const uniformData = new Float32Array(4); // [angle, 0, 0, 0]
// 用 layout:'auto' 时,直接从管线反查布局
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{ binding: 0, resource: { buffer: uniformBuffer } }],
});
function frame(time) {
uniformData[0] = time * 0.001;
device.queue.writeBuffer(uniformBuffer, 0, uniformData);
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({ /* ...同上... */ });
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup); // 一次调用,整体切换
pass.setVertexBuffer(0, vertexBuffer);
pass.draw(3);
pass.end();
device.queue.submit([encoder.finish()]);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
差异一目了然 :WebGL 是"每次 draw 前逐个设状态",WebGPU 是"预先把资源打包成绑定组,一次 setBindGroup 整体切换"。后者在资源数量多时 CPU 开销低得多。
七、WebGPU 的独门兵器:计算着色器
这是 WebGPU 真正无法被 WebGL 替代的能力------在 GPU 上跑通用计算,不经过任何渲染环节。粒子模拟、物理求解、图像后处理、浏览器端 AI 推理(Transformers.js、WebLLM、ONNX Runtime Web)都靠它。
下面是个最小可运行例子:把一个数组每个元素翻倍,然后读回 CPU。
js
const data = new Float32Array([1, 2, 3, 4, 5, 6, 7, 8]);
// 存储缓冲:GPU 可读写 + 可拷贝出去
const storageBuffer = device.createBuffer({
size: data.byteLength,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(storageBuffer, 0, data);
// 回读缓冲:CPU 可映射读取
const readbackBuffer = device.createBuffer({
size: data.byteLength,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
const module = device.createShaderModule({ code: `
@group(0) @binding(0) var<storage, read_write> data : array<f32>;
@compute @workgroup_size(8)
fn main(@builtin(global_invocation_id) gid : vec3u) {
let i = gid.x;
data[i] = data[i] * 2.0;
}
`});
const computePipeline = device.createComputePipeline({
layout: 'auto',
compute: { module, 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);
pass.dispatchWorkgroups(1); // 派发 1 个工作组 × 8 个线程 = 8 个元素
pass.end();
encoder.copyBufferToBuffer(storageBuffer, 0, readbackBuffer, 0, data.byteLength);
device.queue.submit([encoder.finish()]);
// 读回(注意:mapAsync 是异步的,且用前必须 unmap)
await readbackBuffer.mapAsync(GPUMapMode.READ);
const result = new Float32Array(readbackBuffer.getMappedRange().slice(0));
readbackBuffer.unmap();
console.log(result); // [2, 4, 6, 8, 10, 12, 14, 16]
7.1 三个必须理解的概念
- 工作组(workgroup) :一组并行线程,
@workgroup_size(8)表示每个工作组 8 个线程; - 派发(dispatch) :
dispatchWorkgroups(1)表示派发 1 个工作组,总线程数 = 1 × 8 = 8; - 全局 ID :
global_invocation_id是线程在整次派发中的唯一索引,用它定位自己该处理哪个数据。
可以理解为:总线程数 = dispatchWorkgroups 参数 × workgroup_size。
八、核心差异总对照表
| 维度 | WebGL 2 | WebGPU |
|---|---|---|
| 底层映射 | OpenGL ES 3.0 | Vulkan / Metal / D3D12 |
| 首次发布 | 2017(1.0 为 2011) | 2023(Chrome 113) |
| 着色器语言 | GLSL ES 3.00 | WGSL |
| 状态模型 | 全局状态机 | 管线对象 + 绑定组 |
| 命令提交 | 立即模式 | 命令缓冲,批量提交 |
| 多线程 | 只能在主线程 | 命令可在 Worker 录制 |
| 计算着色器 | ❌(只能用片元着色器模拟) | ✅ 原生支持 |
| 顶点属性配置 | vertexAttribPointer 逐个设 |
顶点缓冲布局在管线里声明 |
| Uniform | gl.uniformXX() 逐个设 |
uniform 缓冲 + 绑定组 |
| 错误处理 | getError() 轮询 |
pushErrorScope / device lost Promise |
| 画布格式 | 固定 | getPreferredCanvasFormat() |
| 特性扩展 | 大量 EXT_* 扩展 |
可选特性 adapter.features |
| 学习曲线 | 平缓,资料多 | 陡峭,样板代码多 |
| 浏览器覆盖 | ~99% | ~85% |
九、我该怎么选:决策矩阵与迁移路径
9.1 决策矩阵
| 场景 | 建议 |
|---|---|
| 新项目、无历史包袱 | 直接上 WebGPU,保留 WebGL 2 回退 |
| 大量 draw call / 实例化渲染 / 粒子系统 | 尽快迁移,收益明显 |
| 需要 GPU 计算(模拟、AI 推理、GPU 剔除) | 必须 WebGPU |
| 现有项目运行流畅、无新需求 | 继续 WebGL,不必折腾 |
| 大量自定义 GLSL 着色器 | 先评估改写成本(WGSL 或 TSL) |
| 面向 Linux / Firefox 为主的受众 | 以 WebGL 2 为主,WebGPU 作为增强 |
| 简易展示型 3D(旋转立方体级别) | 两者皆可,WebGL 更快出活 |
9.2 用 Three.js 迁移:几乎是一行代码
如果你不想手写 WGSL,主流引擎已经把回退逻辑做好了:
js
import * as THREE from 'three';
import { WebGPURenderer } from 'three/webgpu';
const renderer = new WebGPURenderer({ antialias: true });
await renderer.init(); // ⚠️ 必须等待异步初始化
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
// 后续 API 与 WebGLRenderer 完全一致
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 1000);
renderer.setAnimationLoop(() => renderer.render(scene, camera));
关键点:
WebGPURenderer来自three/webgpu,检测到不支持时自动降级到 WebGL 2 后端,"一套代码两个后端";- 用
renderer.backend.isWebGPUBackend判断当前跑在哪个后端,据此决定是否启用计算着色器相关效果; - 自定义的
ShaderMaterial(GLSL)不会自动转换,需要改写成 TSL(Three Shading Language) 节点材质,这是迁移中最大的一块工作量; - 开发期可以用
forceWebGL: true强制走回退路径,提前测试降级表现。
9.3 渐近增强的标准写法
js
async function createRenderer(canvas) {
if (navigator.gpu) {
try {
const adapter = await navigator.gpu.requestAdapter();
if (adapter) {
const r = new WebGPURenderer({ canvas, antialias: true });
await r.init();
return { renderer: r, backend: 'webgpu' };
}
} catch (e) {
console.warn('WebGPU 初始化失败,降级 WebGL 2:', e.message);
}
}
return { renderer: new THREE.WebGLRenderer({ canvas, antialias: true }), backend: 'webgl2' };
}
9.4 手写 WebGPU 的架构建议
如果不用引擎、直接写原生 WebGPU,建议按这个顺序搭骨架:
- 初始化层:adapter → device → 画布 configure,统一处理 device lost;
- 资源层:所有 buffer / texture 集中创建,usage 一次声明完整;
- 管线层 :渲染管线与计算管线在初始化时一次性创建(不要在渲染循环里创建管线,这是最大的性能陷阱);
- 绑定层:每帧变化的 uniform 用一个或多个绑定组管理;
- 帧循环层 :每帧
createCommandEncoder→ 录制 →submit。
十、调试与性能工具
| 工具 | 用途 |
|---|---|
chrome://gpu |
查看 WebGPU 是否被硬件加速、GPU 是否被黑名单 |
| Chrome DevTools | 控制台会输出带行号的 WGSL 编译错误 |
| WebGPU Inspector(Chrome 扩展) | 抓取 GPU 调用、查看资源 |
| RenderDoc | 支持 WebGPU 帧捕获,做深度性能分析 |
| Spector.js | WebGL 抓帧神器,看状态机变化 |
device.pushErrorScope() |
精确定位是哪一次 GPU 调用出错 |
| stats-gl / stats.js | 帧率与内存监控 |
性能第一条 :WebGPU 的收益来自"减少 CPU 端开销",所以真正该优化的是管线创建次数、绑定组切换次数、命令缓冲录制方式,而不是着色器里的某行数学运算。
十一、常见报错速查
| 报错 / 现象 | 原因 | 解决办法 |
|---|---|---|
| WebGL 黑屏,控制台无报错 | 着色器编译失败但未检查 | 加 COMPILE_STATUS 检查 |
navigator.gpu 存在但 requestAdapter() 返回 null |
驱动被列入黑名单 / 无可用 GPU | 走 WebGL 回退,不要反复重试 |
| WebGPU 报 "canvas not configured" | 忘了 context.configure() |
在绘制前配置画布上下文 |
| 画面不更新 | 缓存了 getCurrentTexture() 的结果 |
每帧重新调用 |
| 创建 buffer 报 usage 错误 | usage 未包含所需用途 | 创建时一次声明全(COPY_DST 等) |
| uniform 数据错乱 | uniform buffer 未按 16 字节对齐 | 显式补 padding 或按 vec4 打包 |
device lost 频繁出现 |
显存耗尽 / 驱动 TDR 超时(约 2 秒) | 关闭高显存占用的后台标签页、更新驱动、拆分计算任务 |
file:// 打开无法初始化 |
非安全上下文 | 用 localhost 或 https 起服务 |
十二、学习资源
- 官方规范 :WebGPU 规范 · WGSL 规范
- 系统教程 :WebGPU Fundamentals(含大量可运行示例)
- 中文站点 :从零学会 WebGPU(中文教程 + API 参考)
- MDN :WebGPU API
- 实现状态 :gpuweb Implementation Status
- 支持率查询 :caniuse.com/webgpu
- 引擎文档 :Three.js
WebGPURenderer/ Babylon.js WebGPU 引擎
十三、小结
- WebGL 是 OpenGL ES 的 Web 绑定,WebGPU 是现代原生 GPU API 的 Web 封装,两者没有继承关系,会长期共存。
- 心智模型是通用的:数据进显存 → 着色器描述计算 → 光栅化 → 写画布。变的只是"怎么把这些告诉 GPU"。
- WebGL 的核心动作是改状态,WebGPU 的核心动作是建对象。理解这一点,API 的记忆量减半。
- 计算着色器是 WebGPU 的护城河,也是浏览器端 AI 推理得以落地的基础。
- 2026 年的正确姿势:WebGPU 为主路径 + WebGL 2 自动回退,用 Three.js 等引擎可以几乎零成本获得这套能力。
- 别忘了检测分两步 :
navigator.gpu存在不等于能用,必须requestAdapter()成功才算数。
如果这篇文章对你有帮助,欢迎点赞收藏 🌹
标签:WebGL · WebGPU · WGSL · 图形学 · 前端 · 计算着色器
本文浏览器支持数据截至 2026 年 10 月,请以 caniuse.com/webgpu 的最新数据为准。