跳到主要内容

WebGPU 入门:在浏览器中计算数组与绘制三角形

WebGPU 让网页既能用图形处理器(GPU)绘图,也能执行通用的并行计算,例如批量处理图像像素或更新模拟中的粒子。JavaScript 负责准备数据和提交任务,GPU 上的小程序用 WGSL(WebGPU Shading Language,WebGPU 着色语言)编写。这个小程序通常称为“着色器”,但计算着色器不需要颜色、图像或画布。MDN 的 WebGPU 概览区分了计算管线与渲染管线的用途。

本文先把 [1, 2, 3, 4, 5] 交给 GPU,读回 [2, 4, 6, 8, 10],再用独立页面绘制一个三角形。你需要能看懂基本 JavaScript 数组和 async/await,并准备浏览器、文本编辑器和本地静态服务器。无需打包器、npm 依赖、账号、CUDA 安装或云 API。

想先看到结果,可以从“在本地运行数组计算”开始,再回头理解资源模型。两个示例都是可独立保存的 HTML 文件,不需要修改网站组件。

什么工作适合交给 GPU

中央处理器(CPU)擅长处理低延迟的控制逻辑、复杂分支和前后依赖较强的任务。CPU 也有多核和向量运算能力,不能简单理解为“CPU 串行、GPU 并行”。GPU 的优势场景是让大量执行单元对许多元素进行相似操作;能否充分利用它,取决于数据规模、访存方式和算法的依赖关系。

例如,数组每个元素乘二,彼此没有依赖,可以同时计算。但创建设备、编译着色器、建立管线、上传数据、安排执行和读回结果都需要工作。对五个数做一次乘法,普通 JavaScript 往往是更合理的选择。本例用于看清正确的数据流,不用于证明加速。

技术与 WebGPU 的关系
WebGPU面向浏览器的绘图与计算 API,显式管理资源和命令,使用 WGSL 编写 GPU 程序。
WebGL沿用 OpenGL ES 模型的浏览器图形 API。可以用图形技巧表达部分计算,但不是 WebGPU 的独立计算管线。旧 WebGL 程序不能直接当作 WebGPU 程序使用。
CUDANVIDIA 的原生 GPU 计算生态。CUDA 程序不会直接变成浏览器中的 WGSL,WebGPU 也不提供任意 CUDA API。
Vulkan、Metal、Direct3D 12原生图形与计算 API,浏览器实现可以把 WebGPU 工作交给这些后端。网页面对的仍是 WebGPU 的验证规则、能力和着色器语言,不应假定特定后端。

Chrome 的 WebGPU 介绍说明了它与现代原生 GPU API 的关系。选择浏览器 API 还是原生计算生态,首先取决于程序要在哪里运行,而不是未经测量的性能排名。

先检查实际能力,再看浏览器版本

WebGPU 需要安全上下文。通常使用 HTTPS;本地开发时,http://localhost:8000http://127.0.0.1:8000 也属于可被信任的本地来源。普通 HTTP 局域网地址不等于 localhost。本文统一使用本地服务器,不以双击 file:// 文件作为操作路径。

按顺序检查:

  1. window.isSecureContext 是否为真。
  2. 是否有 navigator.gpu
  3. requestAdapter() 是否返回非空 adapter。
  4. adapter.requestDevice() 是否成功完成。
  5. 创建的 device.featuresdevice.limits 是否满足实际任务。

Adapter 可以理解为浏览器选出的 GPU 能力入口;返回 null 表示没有获得可用入口,不能仅凭这个值断定“驱动坏了”。浏览器配置、运行环境、设备与驱动、所需能力以及资源状态都可能影响可用性。设备创建失败也必须捕获,不能把“属性存在”当作初始化成功。MDN 的初始化说明给出了这条检查路径。

官方发布记录能告诉我们什么

以下是截至 2026-09-12 查阅资料时可引用的发布节点,不是覆盖所有设备的当前支持表:

  • Chrome 概览记录了 Chrome 113 在使用 Vulkan 的 ChromeOS、使用 Direct3D 12 的 Windows 和 macOS 上的首批支持;Android 的节点是 Chrome 121、Android 12 及以上、Qualcomm 或 ARM GPU。
  • 同一概览列出了 Firefox 141 在 Windows 上的支持,以及 Safari 26 的发布节点。这些信息不能外推为所有 Firefox 平台或所有 Apple 系统、设备均可用。
  • 概览中 Linux “即将支持”的表述较旧。Chrome 144 的官方更新已经描述了从 Intel Gen12 及更新 GPU 开始的谨慎 Linux 推出过程,并说明 WebGPU 使用 Vulkan。这是逐步推出的记录,不代表全部 Linux 显卡都能使用。

不必根据这份历史列表猜测自己的机器,也不要默认开启实验标志或降低安全设置。以上运行时检查才决定当前页面能否使用 WebGPU;面向用户的产品还应在目标浏览器、操作系统和设备上测试。

可选功能与限制

adapter.featuresadapter.limits 表示可申请的能力;device.featuresdevice.limits 表示创建出的设备实际暴露的能力。可选功能必须先检查,再放进 requiredFeatures;确有需要时才通过 requiredLimits 请求支持的限制值。请求无法满足的能力会使设备创建失败,具体契约见 requestDevice()

不要一次申请所有功能和最大限制。本例不要求任何可选功能。扩大数据量时,再按需要检查缓冲区大小、storage binding 大小、工作组与派发数量等限制。也不要把所有限制理解成“越大越好”:最大容量与偏移对齐要求的比较语义不同。

读代码前,认识这些对象

对象或概念在程序中负责什么
Adapter提供浏览器选出的实现能力,不保证对应某个指定的物理 GPU。
Device创建资源、管线和队列所用的逻辑设备连接。
Queue接收上传和命令提交。提交只是安排执行,不表示 CPU 已能读取结果。
Buffer按字节存放的数据区域;创建时声明允许的用途,CPU 与着色器必须约定一致的布局。
Shader module / WGSLGPU 程序及其语言;计算、顶点和片元入口承担不同任务。
Pipeline(管线)把着色器入口、资源布局及必要的渲染状态组合为可执行配置。
Bind group(绑定组)把具体资源接到着色器声明的编号上;布局规定这些资源的类型。
Command encoder(命令编码器)记录操作,finish() 后得到可提交的命令缓冲区。
Pass一段计算或渲染命令。先结束 pass,才能记录顶层的缓冲区复制。
Canvas context渲染结果的呈现目标;计算程序可以完全不使用它。

可以把本例的顺序记成:分配 → 上传 → 绑定 → 记录计算 → 结束 pass → 记录复制 → 提交 → 等待读回 → 释放。JavaScript 不会为每个数组元素调用一次回调;一次派发会在 GPU 上启动许多 WGSL 执行实例。对象间的详细契约见 WebGPU API

在本地运行数组计算

在一个新的学习目录中,把下面的完整内容保存为 index.html。保留 .html 扩展名,不要保存成 .html.txt。示例中的状态和错误消息保留英文,便于与 API 文档对照。

它把输入上传到 storage buffer,在原处乘二,再复制到专门用于 CPU 读取的 staging buffer。这里的 staging 是中转区域,不是另一次计算。失败时页面会明确显示 CPU 回退结果,不会把它冒充 GPU 输出。

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>WebGPU: double an array</title>
<h1>WebGPU compute</h1>
<pre id="output" role="status">Starting…</pre>
<script type="module">
const output = document.querySelector('#output');
const input = new Float32Array([1, 2, 3, 4, 5]);
const WORKGROUP_SIZE = 64;

async function main() {
let device;
let storage;
let staging;
let scopeOpen = false;
let asyncFailure = null;

try {
if (!window.isSecureContext) {
throw new Error('Use HTTPS or a localhost static server.');
}
if (!navigator.gpu) {
throw new Error('WebGPU is unavailable in this browser/context.');
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
throw new Error('No usable WebGPU adapter was returned.');
}
device = await adapter.requestDevice();
device.lost.then(info => {
if (info.reason !== 'destroyed') {
asyncFailure = new Error(`GPU device lost: ${info.message}`);
output.textContent = asyncFailure.message;
}
});
device.addEventListener('uncapturederror', event => {
asyncFailure = new Error(event.error.message);
output.textContent = `GPU error: ${event.error.message}`;
});

device.pushErrorScope('validation');
scopeOpen = true;
storage = device.createBuffer({
label: 'In-place float data',
size: input.byteLength,
usage: GPUBufferUsage.STORAGE |
GPUBufferUsage.COPY_DST |
GPUBufferUsage.COPY_SRC,
});
staging = device.createBuffer({
label: 'CPU readback',
size: input.byteLength,
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});
device.queue.writeBuffer(storage, 0, input);

const module = device.createShaderModule({
label: 'Double each float',
code: `
@group(0) @binding(0)
var<storage, read_write> values: array<f32>;

@compute @workgroup_size(${WORKGROUP_SIZE})
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
let i = id.x;
if (i >= arrayLength(&values)) {
return;
}
values[i] = values[i] * 2.0;
}
`,
});
const diagnostics = await module.getCompilationInfo();
const shaderErrors = diagnostics.messages.filter(m => m.type === 'error');
if (shaderErrors.length) {
throw new Error(shaderErrors.map(m =>
`${m.lineNum}:${m.linePos} ${m.message}`).join('\n'));
}
const pipeline = await device.createComputePipelineAsync({
layout: 'auto',
compute: {module, entryPoint: 'main'},
});
const bindings = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{binding: 0, resource: {buffer: storage}}],
});

const encoder = device.createCommandEncoder();
const pass = encoder.beginComputePass();
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindings);
pass.dispatchWorkgroups(Math.ceil(input.length / WORKGROUP_SIZE));
pass.end();
encoder.copyBufferToBuffer(storage, 0, staging, 0, input.byteLength);
device.queue.submit([encoder.finish()]);

const scopeResult = device.popErrorScope();
scopeOpen = false;
const validationError = await scopeResult;
if (validationError) throw new Error(validationError.message);

await staging.mapAsync(GPUMapMode.READ);
// Copy before unmap: the mapped ArrayBuffer is detached by unmap().
const result = new Float32Array(staging.getMappedRange().slice(0));
staging.unmap();
if (asyncFailure) throw asyncFailure;

const correct = result.every((value, i) => value === input[i] * 2);
if (!correct) throw new Error('Result verification failed.');
output.textContent =
`Input: ${JSON.stringify(Array.from(input))}\n` +
`GPU: ${JSON.stringify(Array.from(result))}\nVerified.`;
} catch (error) {
// Explicit educational fallback, never presented as a GPU result.
const cpu = input.map(value => value * 2);
output.textContent =
`WebGPU did not complete: ${error.message ?? String(error)}\n` +
`CPU fallback: ${JSON.stringify(Array.from(cpu))}`;
} finally {
if (scopeOpen && device) {
try { await device.popErrorScope(); } catch { /* Original error shown. */ }
}
if (staging?.mapState === 'mapped') staging.unmap();
staging?.destroy();
storage?.destroy();
// This is a one-shot demo. A real app normally keeps its device alive.
device?.destroy();
}
}
main();
</script>
</html>

在这个目录打开终端。如果已经安装 Python 3,运行:

python3 -m http.server 8000 --bind 127.0.0.1

Windows 上已有的 Python 安装也可能使用 py -3 -m http.server 8000 --bind 127.0.0.1。已有其他静态服务器也可以使用,无需为本例安装新依赖。只绑定回环地址,避免无意中把目录开放给局域网;目录里只放本次练习文件。

打开 http://127.0.0.1:8000/,正常完成后页面应显示:

Input: [1,2,3,4,5]
GPU: [2,4,6,8,10]
Verified.

如果端口已被占用,把命令和访问地址里的 8000 一起换成 8001。若看到目录列表,检查文件名和启动服务器的目录。结束练习后在终端按 Ctrl+C 停止服务器。

若页面显示 WebGPU did not complete,紧接着的消息说明失败发生在哪里;CPU fallback 只说明普通 JavaScript 得到了结果。先检查地址、安全上下文、浏览器和设备初始化,不要删掉错误处理来“修复”示例。

为什么要这样创建和读取缓冲区

Float32Array 每个元素占 4 字节,五个元素共 20 字节。WGSL 的 array<f32> 在这里使用同样的连续布局。JavaScript 中的 binding 0 对应 WGSL 的 @group(0) @binding(0)layout: 'auto' 方便单管线教学,但不要假定从一个自动布局创建的绑定组可直接用于其他无关管线。

Storage buffer 的三种用途各有作用:STORAGE 允许着色器访问;COPY_DST 允许 queue.writeBuffer() 上传;COPY_SRC 允许把结果复制出去。读回缓冲区只用 COPY_DST | MAP_READ。根据 WebGPU 缓冲区规则,不能把 MAP_READSTORAGE 直接拼到同一个缓冲区上。

mapAsync()完成后,CPU 才能通过映射范围访问数据。本例中偏移量为 0、大小为 20 字节,满足映射偏移是 8 的倍数、大小是 4 的倍数的要求。先复制映射内容再 unmap(),是因为解除映射会使原来的映射视图失效。

submit() 返回不表示 GPU 已算完。这里等 mapAsync() 就足以等待读回,不必额外在它前面调用 queue.onSubmittedWorkDone()。正常路径读完才释放缓冲区;失败路径终止本次任务并释放资源。这个一次性页面最后销毁设备,实际应用通常复用一个设备,而不是每处理一批数据就重新创建。

工作组大小不是派发数量

@workgroup_size(64) 表示每个工作组在 X 方向有 64 个执行实例。dispatchWorkgroups(1) 派发一个这样的组,不是只处理一个元素。五个元素会产生 0–63 的索引,所以 5–63 必须在越界检查处返回。

处理 1,000 个元素时,Math.ceil(1000 / 64) 得到 16 个工作组,共 1,024 个执行实例,最后 24 个返回。64 是便于说明的选择,不是所有硬件上的最优值。检查 maxComputeWorkgroupSizeXmaxComputeInvocationsPerWorkgroupmaxComputeWorkgroupsPerDimension,再决定更大任务如何派发。

每个实例只写自己的元素,因此这里没有同步屏障。保留非空输入;若改成通用函数,应在 CPU 上处理空数组,并在分配前检查字节大小和绑定限制。这个例子里的小整数乘二可以用精确相等验证;一般浮点计算需要根据问题设置误差容限。

一个直接的练习是把运算改成 values[i] = values[i] + 3.0。同时把 JavaScript 验证和 CPU 回退里的乘二改成加三,预期结果是 [4,5,6,7,8]。只改着色器会让正确性检查失败,这正是参考结果应发挥的作用。

再画一个三角形

计算管线只处理数据;渲染管线还要把结果写到图像目标。下面的 triangle.html 是独立完整页面,保存在同一目录后访问 http://127.0.0.1:8000/triangle.html。它绘制一次,并在窗口大小改变时重画,没有连续动画,也不把每帧像素读回 CPU。

<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>WebGPU triangle</title>
<style>
canvas { display: block; width: min(90vw, 640px); height: 360px; }
</style>
<h1>WebGPU triangle</h1>
<pre id="status" role="status">Starting…</pre>
<canvas id="canvas" aria-label="A colored triangle on a dark background">
This example requires canvas support.
</canvas>
<script type="module">
const status = document.querySelector('#status');
const canvas = document.querySelector('#canvas');
let device;
let context;
let frame = 0;
let stopped = false;
let listening = false;
let scopeOpen = false;

function stop() {
stopped = true;
cancelAnimationFrame(frame);
if (listening) window.removeEventListener('resize', scheduleDraw);
context?.unconfigure();
device?.destroy();
}
function fail(error) {
status.textContent = `Cannot render with WebGPU: ${error.message ?? error}`;
stop();
}
let draw = () => {};
function scheduleDraw() {
cancelAnimationFrame(frame);
frame = requestAnimationFrame(() => {
if (!stopped) {
try { draw(); } catch (error) { fail(error); }
}
});
}
window.addEventListener('pagehide', stop, {once: true});

async function main() {
try {
if (!window.isSecureContext || !navigator.gpu) {
throw new Error('Use a supporting browser over HTTPS or localhost.');
}
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) throw new Error('No usable GPU adapter.');
device = await adapter.requestDevice();
if (stopped) { device.destroy(); return; }
device.lost.then(info => {
if (info.reason !== 'destroyed') fail(new Error(`Device lost: ${info.message}`));
});
device.addEventListener('uncapturederror', event => fail(event.error));

context = canvas.getContext('webgpu');
if (!context) throw new Error('No WebGPU canvas context.');
const format = navigator.gpu.getPreferredCanvasFormat();
device.pushErrorScope('validation');
scopeOpen = true;
context.configure({device, format, alphaMode: 'opaque'});
const module = device.createShaderModule({code: `
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) color: vec3<f32>,
}
@vertex
fn vs(@builtin(vertex_index) index: u32) -> VertexOutput {
let positions = array<vec2<f32>, 3>(
vec2<f32>(0.0, 0.7),
vec2<f32>(-0.7, -0.7),
vec2<f32>(0.7, -0.7)
);
let colors = array<vec3<f32>, 3>(
vec3<f32>(1.0, 0.2, 0.2),
vec3<f32>(0.2, 1.0, 0.2),
vec3<f32>(0.2, 0.4, 1.0)
);
var result: VertexOutput;
result.position = vec4<f32>(positions[index], 0.0, 1.0);
result.color = colors[index];
return result;
}
@fragment
fn fs(input: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(input.color, 1.0);
}
`});
const pipeline = await device.createRenderPipelineAsync({
layout: 'auto',
vertex: {module, entryPoint: 'vs'},
fragment: {module, entryPoint: 'fs', targets: [{format}]},
primitive: {topology: 'triangle-list'},
});
const scopeResult = device.popErrorScope();
scopeOpen = false;
const error = await scopeResult;
if (error) throw new Error(error.message);
if (stopped) return;

draw = () => {
const ratio = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
const limit = device.limits.maxTextureDimension2D;
const desiredWidth = Math.max(1, Math.round(rect.width * ratio));
const desiredHeight = Math.max(1, Math.round(rect.height * ratio));
const scale = Math.min(1, limit / desiredWidth, limit / desiredHeight);
const width = Math.max(1, Math.floor(desiredWidth * scale));
const height = Math.max(1, Math.floor(desiredHeight * scale));
if (canvas.width !== width) canvas.width = width;
if (canvas.height !== height) canvas.height = height;
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: {r: 0.03, g: 0.04, b: 0.08, a: 1},
loadOp: 'clear',
storeOp: 'store',
}],
});
pass.setPipeline(pipeline);
pass.draw(3);
pass.end();
device.queue.submit([encoder.finish()]);
};
window.addEventListener('resize', scheduleDraw);
listening = true;
draw();
status.textContent = 'Triangle submitted. Resize the window to redraw.';
} catch (error) {
fail(error);
} finally {
if (scopeOpen && device) {
try { await device.popErrorScope(); } catch { /* Failure shown above. */ }
}
}
}
main();
</script>
</html>

预期画面是深色背景上的彩色三角形。Triangle submitted 表示命令已提交,并不单凭这条消息证明屏幕呈现成功。没有可用 GPU 时页面显示明确错误;这个渲染示例没有另行实现 WebGL 回退。

顶点、片元和画布分别做什么

顶点着色器 vsvertex_index 生成三个顶点的位置和颜色。位置使用裁剪空间坐标;这里 w = 1,X 和 Y 的 -1 到 1 对应目标区域的两端。为了让几何过程清楚,这个小例子把三个点写在着色器中,不需要顶点缓冲区或绑定组。

光栅化会为三角形覆盖的区域生成片元,并插值顶点颜色;片元着色器 fs 返回该位置的颜色。片元是渲染过程中的候选贡献,不能在所有情况下都等同于最终屏幕上的一个像素。draw(3) 让顶点阶段处理三个顶点,triangle-list 把它们组成三角形。

getPreferredCanvasFormat()返回当前系统推荐的画布格式。管线里的 targets: [{format}] 要与画布一致,不要固定使用某台桌面机器上的格式。每次绘制都重新取得 getCurrentTexture(),不要把一次取得的呈现纹理永久缓存。WebGPU 画布规范说明了配置与呈现纹理的生命周期。

CSS 宽高决定页面布局,canvas.widthcanvas.height 决定背后实际绘制的像素数。代码按 devicePixelRatio 放大像素尺寸,再按 maxTextureDimension2D 等比例限制。loadOp: 'clear' 配合 clearValue 清背景;storeOp: 'store' 保留结果用于呈现。

这个固定页面只监听窗口 resize。嵌入复杂布局时,还需处理元素自身尺寸与像素比例的变化。本例在 pagehide 时清理设备;若页面通过浏览器前进后退缓存恢复,应重新初始化,练习时也可直接重新加载。不要直接把这段一次性页面生命周期当作完整组件框架。

数据布局:JavaScript 数组必须与 WGSL 对齐

第一个例子的标量数组没有额外填充,但三维坐标常会遇到布局问题。WGSL 对齐与大小规范规定如下 storage 元素布局,单位均为字节:

WGSL storage 元素对齐大小数组步长
f32444
vec3<f32>161216

“大小”是值本身占用的空间;“对齐”限制起始地址;“步长”是相邻数组元素起点之间的距离。数组步长按元素对齐要求向上补齐,所以 array<vec3<f32>> 中每个点应在主机侧占四个浮点槽位,而不是三个:

const points = new Float32Array([
1, 2, 3, 0, // x, y, z, padding
4, 5, 6, 0,
]);

第四个槽位是填充,不是 WGSL vec3 的第四个分量。包含两个 vec3f 成员的结构体,成员偏移为 0 和 16,总大小为 32;更多例子见 WGSL 数组布局说明。Uniform 地址空间还有额外规则,不能把 storage 打包方式直接照搬过去。

结构成员对齐、数组步长、buffer binding 偏移对齐、映射范围对齐是不同约束。数据值奇怪或验证失败时,先按字节检查布局和实际绑定范围,不要只看数组元素数量。数值程序还要留意 WGSL f32 与 JavaScript 默认 Number 的精度差异。

扩大任务时,先处理同步与错误

工作组内的屏障只能同步该组参与的执行实例,不能让所有工作组在某处集合。需要跨组通信的算法通常拆成多次有顺序的派发。若加了 workgroupBarrier(),不要未经分析就保留“部分实例提前返回、其他实例到达屏障”的控制流;WGSL 对屏障所在控制流有一致性要求。相关规则见 WGSL 一致性分析

在 GPU 仍需要使用某个缓冲区时,不要把同一个缓冲区保持为映射状态。反复运行时复用管线和资源,把中间数据留在 GPU,CPU 确实需要时才读回。多个中转缓冲区可以用于更复杂的重叠调度,但不是入门例子正确运行的前提。

现象应检查或采取的动作
没有 navigator.gpu检查安全上下文和目标浏览器;显示不可用信息,并明确选择 CPU 或其他渲染方案。
Adapter 为 null 或设备请求被拒绝停止初始化,显示具体错误;不能据此判断单一硬件故障,也不要无上限重试。
着色器编译失败getCompilationInfo() 查看行列和消息;计算示例会把这些诊断显示出来。
Validation error核对用途标志、绑定类型、大小、布局、pass 是否结束及资源是否已失效。
映射失败或结果不对检查复制是否已编码和提交、读回用途与范围,以及是否在解除映射前复制了数据。
设备丢失停止提交任务;需要恢复时创建新设备,并重新创建所有依赖它的资源。

错误作用域 pushErrorScope() / popErrorScope() 捕获所选类别的异步 GPU 错误;popErrorScope() 返回首个匹配错误或 null。示例只捕获 validation,不代表捕获了内存不足和内部错误等所有类别;uncapturederror 监听器处理未被作用域捕获的错误。普通 JavaScript try/catch 本身不能取代这些机制,详见 MDN 错误处理说明

device.lost 即使在刚创建设备后也可能完成。设备恢复不能复用旧设备的缓冲区、纹理、绑定组或管线。主动调用 device.destroy() 也会触发丢失通知,因此代码跳过 reason === 'destroyed',避免把正常清理当成恢复请求。

不用的自有缓冲区和纹理应及时销毁,映射应解除。管线和绑定组没有通用的 .destroy() 方法,释放引用即可。画布当前呈现纹理由浏览器管理,不应把它当作自行分配、永久持有的纹理。

怎样判断是否真的更快

先写出正确的 CPU 参考实现,再选择真实输入规模。测量至少分清两个问题:首次打开页面多久得到结果,以及复用设备、管线和缓冲区后,一批工作多久完成。

  1. 把设备初始化与着色器、管线创建成本单独记录,不与热运行混为一谈。
  2. 端到端比较使用同样的计时范围,包括需要的上传、计算、复制、等待、读回和结果处理。
  3. 只在 queue.submit() 前后计时,测到的主要是提交过程。第一个例子若要测“JavaScript 何时拿到结果”,应计时到 await mapAsync() 及结果复制完成。
  4. 重复测量并报告波动、数据规模、浏览器、操作系统和 GPU 条件,不凭一次观察宣布普遍加速。
  5. 把正确性验证放在适当位置,数值任务说明浮点误差容限;不要为了得到更好看的时间省略必要的数据传输。

这些建议来自上述提交与读回过程,不构成任何速度承诺。可选的 timestamp-query 可用于更细的 GPU 时间分析,但要另行检查和请求支持,也不能代替端到端时间。计算后直接渲染、不读回 CPU,是另一种有价值的工作负载,应与“每次结果都返回 JavaScript”的任务分别测量。

适合的应用与继续练习

图像滤镜、大批粒子更新、规则数值数组、部分模拟内核和浏览器端机器学习推理,都值得评估 WebGPU:它们可能有足够多的独立工作来分摊调度和传输成本。这是选题方向,不是已测得的加速结论。机器学习还要核对所用框架、算子和目标浏览器;科学计算要先确认精度、算法依赖、内存容量和结果验证要求。

少量数组、DOM 操作、UI 状态、强串行逻辑和不断往返的小结果通常更适合普通 JavaScript 或 CPU worker。浏览器提供的能力、内存或客户设备覆盖面不够时,再考虑原生程序或服务器。WebGPU 不提供直接操作 DOM 的能力,也不会绕过浏览器沙箱。

Site Lab 提供了可对照的现有实验:棱镜色散使用 p5.js 批量 WebGL 绘制,ASCII 地球展示了 Three.js 场景如何变成字符,Rössler 吸引子连接数值积分与轨迹呈现。它们是学习对照,不是已经转换为 WebGPU 的实现。

可先把数组例子扩展为一批彼此独立的粒子更新,再尝试让渲染阶段直接使用计算结果,避免每帧读回。比较 Rössler 模型时要保留时间步之间的依赖:一条轨迹的下一步需要上一步结果,不能把所有时间步简单独立派发;多条不同初始条件的轨迹才可能提供另一层并行性。更多开发与项目工具见工具与工作流

探索关联打开关联网络