WebGPU Compute Shaders实战:浏览器端GPU计算入门与优化

前端工程浏览器 AI 与 WebAssembly

WebGPU Compute Shaders:被忽视的GPU计算杀手锏

当大多数人谈论WebGPU时,第一反应总是"WebGL的替代品"。但如果你只把WebGPU当成新一代图形API来用,那就大错特错了——Compute Shader才是WebGPU真正的杀手锏

WebGPU的Compute Shader让浏览器首次拥有了通用GPU计算(GPGPU)的原生能力,这不是渐进式改进,而是范式转换。

WebGL时代要做GPGPU,只能把数据塞进纹理,用片段着色器模拟计算,再把结果读回CPU。这种"曲线救国"的方式既别扭又低效。WebGPU的Compute Shader直接打破了这一限制,让GPU真正成为浏览器的并行计算引擎。


WGSL着色器语言核心语法

WebGPU引入了全新的着色器语言WGSL(WebGPU Shading Language),取代了GLSL。来看核心语法:

变量与类型

// 标量类型
var f32_val: f32 = 3.14;
var i32_val: i32 = 42;
var u32_val: u32 = 100u;

// 向量与矩阵
var vec3_val: vec3<f32> = vec3<f32>(1.0, 2.0, 3.0);
var mat4_val: mat4x4<f32> = mat4x4<f32>();

// 存储缓冲区(Compute Shader的核心数据结构)
@group(0) @binding(0) var<storage, read> inputData: array<f32>;
@group(0) @binding(1) var<storage, read_write> outputData: array<f32>;

Compute Shader入口函数

@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
    let idx = global_id.x;
    if (idx >= arrayLength(&inputData)) {
        return;
    }
    outputData[idx] = inputData[idx] * 2.0;
}

关键概念:Workgroup Size

@workgroup_size(64)声明每个工作组的线程数为64。GPU以工作组为单位调度执行,合理设置workgroup size对性能至关重要:

硬件 推荐Workgroup Size 说明
NVIDIA 128-256 Warp大小为32,取其倍数
AMD 64-128 Wavefront大小为64
Intel 32-64 子组大小较小
通用安全值 64 兼容所有硬件

Compute Shader vs WebGL Transform Feedback

维度 WebGL Transform Feedback WebGPU Compute Shader
编程模型 伪装成渲染管线的计算 原生通用计算
数据传递 纹理/VBO Storage Buffer
线程控制 顶点数模拟 精确的dispatch控制
数据回读 readPixels极慢 mapAsync高效
随机写入 不支持 原子操作+随机写入
共享内存 Workgroup Shared Memory
调试 几乎不可能 Chrome DevTools支持

Transform Feedback本质上是用图形管线"蹭"计算能力,而Compute Shader是GPU为计算而生的正途。


实战一:Compute Shader实现图像高斯模糊

为什么用Compute Shader做图像处理?

JavaScript处理一张1080p图片的高斯模糊需要数百毫秒,而Compute Shader可以在5ms内完成——快了约50倍。

完整实现

// gaussian_blur.wgsl
@group(0) @binding(0) var srcTexture: texture_2d<f32>;
@group(0) @binding(1) var dstTexture: texture_storage_2d<rgba8unorm, write>;
@group(0) @binding(2) var<uniform> params: Params;

struct Params {
    width: u32,
    height: u32,
    radius: f32,
}

fn gaussianWeight(x: f32, sigma: f32) -> f32 {
    return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * 3.14159265) * sigma);
}

@compute @workgroup_size(16, 16)
fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
    let pixel_coords = vec2<u32>(global_id.x, global_id.y);
    if (pixel_coords.x >= params.width || pixel_coords.y >= params.height) {
        return;
    }

    let sigma = params.radius / 3.0;
    var color = vec4<f32>(0.0);
    var total_weight = 0.0;

    let radius_i = i32(params.radius);
    for (var dx: i32 = -radius_i; dx <= radius_i; dx++) {
        for (var dy: i32 = -radius_i; dy <= radius_i; dy++) {
            let weight = gaussianWeight(f32(dx), sigma) * gaussianWeight(f32(dy), sigma);
            let sample_coords = vec2<i32>(
                clamp(i32(pixel_coords.x) + dx, 0, i32(params.width) - 1),
                clamp(i32(pixel_coords.y) + dy, 0, i32(params.height) - 1)
            );
            color += textureLoad(srcTexture, vec2<u32>(sample_coords), 0) * weight;
            total_weight += weight;
        }
    }

    textureStore(dstTexture, pixel_coords, color / total_weight);
}

JavaScript端Pipeline设置

const pipeline = device.createComputePipeline({
    layout: 'auto',
    compute: {
        module: device.createShaderModule({ code: wgslCode }),
        entryPoint: 'main',
    }
});

const bindGroup = device.createBindGroup({
    layout: pipeline.getBindGroupLayout(0),
    entries: [
        { binding: 0, resource: srcTexture.createView() },
        { binding: 1, resource: dstTexture.createView() },
        { binding: 2, resource: { buffer: paramsBuffer } },
    ]
});

const commandEncoder = device.createCommandEncoder();
const passEncoder = commandEncoder.beginComputePass();
passEncoder.setPipeline(pipeline);
passEncoder.setBindGroup(0, bindGroup);
passEncoder.dispatchWorkgroups(
    Math.ceil(width / 16),
    Math.ceil(height / 16)
);
passEncoder.end();
device.queue.submit([commandEncoder.finish()]);

性能对比

方案 1080p高斯模糊(r=10) 4K高斯模糊(r=10)
JavaScript (单线程) ~450ms ~1800ms
Web Worker (4线程) ~130ms ~520ms
WebGL片段着色器 ~12ms ~45ms
WebGPU Compute Shader ~5ms ~18ms

实战二:大规模粒子模拟(10万粒子60fps)

这是Compute Shader最经典的用例。10万个粒子每帧需要更新位置、速度、碰撞检测,CPU根本扛不住。

粒子数据结构

struct Particle {
    pos: vec2<f32>,
    vel: vec2<f32>,
    color: vec4<f32>,
    life: f32,
}

@group(0) @binding(0) var<storage, read_write> particles: array<Particle>;
@group(0) @binding(1) var<uniform> time: f32;

粒子更新Shader

@compute @workgroup_size(256)
fn updateParticles(@builtin(global_invocation_id) global_id: vec3<u32>) {
    let idx = global_id.x;
    if (idx >= arrayLength(&particles)) { return; }

    var p = particles[idx];

    // 重力
    p.vel.y += -9.8 * 0.016;

    // 阻尼
    p.vel *= 0.999;

    // 位置更新
    p.pos += p.vel * 0.016;

    // 边界碰撞
    if (p.pos.y < 0.0) {
        p.pos.y = 0.0;
        p.vel.y *= -0.7;
    }
    if (p.pos.x < 0.0 || p.pos.x > 1.0) {
        p.vel.x *= -0.7;
        p.pos.x = clamp(p.pos.x, 0.0, 1.0);
    }

    // 生命值衰减
    p.life -= 0.016;
    if (p.life <= 0.0) {
        // 重生粒子
        p.pos = vec2<f32>(0.5, 0.8);
        p.vel = vec2<f32>(
            (fract(sin(f32(idx) * 12.9898) * 43758.5453) - 0.5) * 2.0,
            fract(sin(f32(idx) * 78.233) * 43758.5453) * 3.0
        );
        p.life = 2.0 + fract(sin(f32(idx) * 43.758) * 43758.5453) * 3.0;
    }

    particles[idx] = p;
}

渲染管线:Compute → Render无缝衔接

关键技巧是Compute Shader写入的Storage Buffer直接作为Render Pipeline的顶点数据,零拷贝

// 同一个buffer,compute写入,render读取
const particleBuffer = device.createBuffer({
    size: PARTICLE_COUNT * 6 * 4, // 每个粒子6个float
    usage: GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX,
});

Pipeline布局、Bind Group与Buffer设计模式

推荐的Buffer布局策略

┌─────────────────────────────────────┐
│           Bind Group 0              │  ← 全局资源(每帧更新一次)
│  [0] Uniform Buffer (相机/时间)     │
│  [1] Sampler                       │
│  [2] Texture                       │
├─────────────────────────────────────┤
│           Bind Group 1              │  ← 材质/参数资源
│  [0] Uniform Buffer (参数)          │
│  [1] Storage Buffer (只读数据)      │
├─────────────────────────────────────┤
│           Bind Group 2              │  ← 动态数据
│  [0] Storage Buffer (读写数据)      │
└─────────────────────────────────────┘

Buffer使用标志组合

场景 Usage标志 说明
只读Uniform UNIFORM | COPY_DST 通过writeBuffer更新
只读Storage STORAGE | COPY_DST 大块只读数据
读写Storage STORAGE Compute中间结果
顶点输入 VERTEX | STORAGE Compute→Render零拷贝
暂存Buffer COPY_SRC | COPY_DST | MAP_READ GPU→CPU回读

浏览器兼容性现状

浏览器 Compute Shader支持 版本要求 备注
Chrome ✅ 完整支持 113+ 最成熟的实现
Edge ✅ 完整支持 113+ 与Chrome同内核
Firefox ⚠️ 实验性 Nightly 需手动开启flag
Safari ⚠️ 部分支持 17.4+ macOS/iOS有限支持
Node.js ✅ via wgpu - 服务端GPU计算

特性检测代码

async function checkWebGPUComputeSupport() {
    if (!navigator.gpu) {
        return { supported: false, reason: 'WebGPU API不可用' };
    }
    const adapter = await navigator.gpu.requestAdapter();
    if (!adapter) {
        return { supported: false, reason: '无法获取GPU适配器' };
    }
    const features = adapter.features;
    return {
        supported: true,
        maxWorkgroupSize: adapter.limits.maxComputeWorkgroupSizeX,
        maxStorageBufferSize: adapter.limits.maxStorageBufferBindingSize,
        hasTimestampQuery: features.has('timestamp-query'),
    };
}

与Web Worker + SharedArrayBuffer的协作模式

Compute Shader不是万能的,很多场景需要GPU和CPU协同工作:

架构模式

┌──────────────┐     SharedArrayBuffer     ┌──────────────┐
│  Main Thread │◄─────────────────────────►│  Web Worker  │
│  (WebGPU)    │                            │  (后处理)     │
│  Compute ────┤  mapAsync → SharedAB ─────►│  数据分析     │
│  Shader      │                            │  决策逻辑     │
└──────────────┘                            └──────────────┘

实际代码

// 主线程:运行Compute Shader
const readBuffer = device.createBuffer({
    size: resultSize,
    usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ,
});

// ... compute pass ...

// 异步回读结果
await readBuffer.mapAsync(GPUMapMode.READ);
const resultData = new Float32Array(readBuffer.getMappedRange().slice(0));

// 写入SharedArrayBuffer,通知Worker
const sharedBuffer = new Float32Array(sharedArrayBuffer);
sharedBuffer.set(resultData);
Atomics.notify(sharedInt32, 0);

// Worker端:读取计算结果做决策
Atomics.wait(workerInt32, 0, 0);
const computedResult = new Float32Array(sharedArrayBuffer);
// 基于GPU计算结果做CPU端决策...

这种模式特别适合"GPU做重计算,CPU做逻辑决策"的场景,比如物理模拟中GPU算碰撞,CPU做游戏逻辑。


总结与展望

WebGPU Compute Shader为Web平台带来了真正的GPU通用计算能力。2026年的今天,Chrome 113+已经稳定支持,生态工具链也在快速成熟。

核心要点回顾:

  • Compute Shader是WebGPU区别于WebGL的核心能力
  • WGSL语法简洁,学习成本低于GLSL
  • 图像处理场景可获得50x性能提升
  • 10万粒子模拟在浏览器中轻松60fps
  • 合理的Bind Group布局是性能优化的关键
  • 与Web Worker协作可以构建完整的异构计算架构

下一步值得关注的方向:WebGPU Subgroups(子组操作,更细粒度的线程协作)和WebGPU Ray Tracing(光线追踪加速结构),这些提案已经在W3C讨论中。

本站提供浏览器本地工具,免注册即可试用 →

#WebGPU#Compute Shader#GPU计算#WGSL#并行计算