在前端世界里,3D
技术这几年越来越热:从智慧城市的三维大屏,到炫酷的官网交互,再到 Web
版小游戏,甚至 AI
+ 可视化的科研项目,都离不开浏览器 3D
渲染。说到前端 3D
,最常被提到的三个名字就是 Three.js
、Babylon.js
和 WebGPU
。它们既有重叠,也有差异,不同开发者用它们时的感受差别很大。
这篇文章不是泛泛的流水账,而是基于实际对比与代码实战 ,帮你认清三者的定位、关系、优缺点。最后,我会用同一个案例 (渐变彩色立方体 + 交互),分别用三种方式实现,并给出可复现的性能测试方法 与选型建议。读完后,你能更清楚地判断:要快出效果 ?要全能引擎 ?还是要底层极致性能?
一、三者到底各是什么?
1. WebGPU
:浏览器的 GPU
底座
WebGPU
是新一代 Web 图形/计算 API ,对应原生的 Vulkan/Metal/D3D12
。相比 WebGL
,它直接暴露现代图形与计算管线,显著降低 JS 侧开销,并把 GPGPU
/机器学习 等场景自然地带到浏览器端(例如 TensorFlow.js
的 WebGPU
后端)。MDN 的官方说明一语中的:WebGPU
是 WebGL
的继任者,提供更快的操作和更先进的 GPU
特性。
浏览器支持(截至 2025-09)

Chrome / Edge
:稳定支持并持续迭代。Firefox
:自 141 版起在 Windows 默认启用,其他平台逐步放量。Safari
:新版本已纳入支持(随平台与版本推进)。
适合场景
追求**极致性能、可控渲染管线、计算着色(GPGPU/AI
推理)**的中长期项目;或希望在 Web 端统一图形/计算栈的团队。
库支持
许多广泛使用的 WebGL
库都在实现 WebGPU
支持,或者已经实现了 WebGPU
支持。这意味着,使用 WebGPU
可能只需要更改一行代码。
Babylon.js
: 完全支持WebGPU
。PlayCanvas
:宣布提供初始WebGPU
支持。TensorFlow.js
:支持大多数运算符的WebGPU
优化版本。Three.js
:WebGPU
支持正在开发中,请参阅示例。
Chromium
的 Dawn
库和 Firefox
的 wgpu
库均可作为独立软件包提供。它们具有出色的可移植性和符合人体工程学的层,可抽象化操作系统 GPU API
。在原生应用中使用这些库,可通过 Emscripten
和 Rust web-sys
更轻松地移植到 WASM
。
2. Three.js
:WebGL
时代的万能 3D 工具箱
Three.js
是生态最大、资料最全、上手最快 的 Web 3D
库。封装完善(几行就能出效果),非常适合可视化 / 官网互动 / 产品展示 / 大屏 等快速产出。近两个大方向是 WebGPURenderer
与 TSL(Three Shader Language)
,目标之一是渲染器无关 的材质/节点系统;但 WebGL
与 WebGPU
渲染器在材质/后效等 API 仍有差异,迁移需灰度验证与实机测试。
3. Babylon.js
:更引擎化的一站式方案
微软团队主导的 Web 3D
引擎,内置相机、动画、粒子、GUI
、物理、XR
等完整子系统,配套编辑器/工具链更工程化 ,适合网页小游戏、沉浸式交互、XR
等偏"引擎式组织"的项目。其 WebGPU
支持有官方状态页可查,迁移/上线前可据此清单验证。
二、它们之间是什么关系?
- 层级 :
WebGPU
在底层 (硬件/驱动映射);Three.js
/Babylon.js
在上层(框架/引擎封装)。 Three.js vs Babylon.js
:同层"竞品"------前者更轻更灵活、社区最大;后者更"引擎",内置能力更全。- 趋势 :上层框架逐步拥抱
WebGPU
,在能用的场景里获得更高可编程性与性能,同时仍保留WebGL
回退 覆盖长尾设备与旧浏览器。

三、共同点与不同点
共同点
- 都能在浏览器里做
3D
场景(相机、网格、材质、动画、交互)。 - 都在持续演进:规范推进 + 浏览器落地 + 库生态跟进。
不同点
维度 | Three.js |
Babylon.js |
WebGPU |
---|---|---|---|
定位 | ``WebGL 封装库(正在接入 WebGPU 后端) |
全功能 3D 引擎(工具链完善) |
原生底层 API(图形 + 计算) |
学习曲线 | 最平滑 | 稍陡(引擎思维) | 最陡(图形/并行/管线) |
可控程度 | 中(灵活够用) | 中上(系统完备) | 最高(细粒度) |
生态/资料 | 最大最活跃 | 官方文档 + 编辑器 + 社区 | 文档完善,但跨浏览器差异需实测 |
现状要点 | WebGPURenderer 实验推进中 |
WebGPU 状态页可查覆盖面 |
标准与实现快速推进中 |
四、先做再说:同一个案例的三种实现
案例设定:
- 同一视觉 :背景
#0B1220
;立方体为渐变彩色 (每顶点颜色 =0.5 + 0.5 * normalize(position)
)。 - 同一交互 :拖拽旋转(
yaw/pitch
)、滚轮缩放(限制距离 1.2~20)。 - 同一相机 :
FOV=60°
,初始distance=3
。
1. Three.js
javascript
import * as THREE from "https://unpkg.com/three@0.161.0/build/three.module.js";
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(devicePixelRatio || 1, 2));
renderer.setSize(innerWidth, innerHeight);
renderer.setClearColor(0x0B1220, 1);
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 0.1, 100);
let yaw = 0, pitch = 0, distance = 3;
function applyCam() {
const ex = distance * Math.sin(yaw) * Math.cos(pitch);
const ey = distance * Math.sin(pitch);
const ez = distance * Math.cos(yaw) * Math.cos(pitch);
camera.position.set(ex, ey, ez);
camera.lookAt(0, 0, 0);
}
applyCam();
// 立方体 + 顶点色(渐变)
const geo = new THREE.BoxGeometry(1, 1, 1);
const pos = geo.getAttribute('position');
const col = new Float32Array(pos.count * 3);
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i), y = pos.getY(i), z = pos.getZ(i);
const l = Math.hypot(x, y, z) || 1;
const nx = x / l, ny = y / l, nz = z / l;
col.set([0.5 + 0.5 * nx, 0.5 + 0.5 * ny, 0.5 + 0.5 * nz], i * 3);
}
geo.setAttribute('color', new THREE.BufferAttribute(col, 3));
const mesh = new THREE.Mesh(geo, new THREE.MeshBasicMaterial({ vertexColors: true }));
scene.add(mesh);
// 交互
let drag = false, lx = 0, ly = 0;
addEventListener('mousedown', e => { drag = true; lx = e.clientX; ly = e.clientY; });
addEventListener('mousemove', e => {
if (!drag) return;
const dx = e.clientX - lx, dy = e.clientY - ly;
lx = e.clientX; ly = e.clientY;
yaw += dx * 0.01;
pitch += dy * 0.01;
const lim = Math.PI / 2 - 0.01;
pitch = Math.max(-lim, Math.min(lim, pitch));
applyCam();
});
addEventListener('mouseup', () => drag = false);
addEventListener('wheel', e => {
e.preventDefault();
const s = e.deltaY > 0 ? 1.1 : 0.9;
distance = Math.min(20, Math.max(1.2, distance * s));
applyCam();
}, { passive: false });
addEventListener('resize', () => {
renderer.setSize(innerWidth, innerHeight);
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
});
(function tick() {
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
requestAnimationFrame(tick);
})();

2. Babylon.js
(引擎式结构)
javascript
const canvas = document.getElementById('c');
const engine = new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true });
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#0B1220FF');
let yaw = 0, pitch = 0, distance = 3;
const camera = new BABYLON.FreeCamera('cam', new BABYLON.Vector3(0, 0, 3), scene);
function applyCam() {
const ex = distance * Math.sin(yaw) * Math.cos(pitch);
const ey = distance * Math.sin(pitch);
const ez = distance * Math.cos(yaw) * Math.cos(pitch);
camera.position.set(ex, ey, ez);
camera.setTarget(BABYLON.Vector3.Zero());
}
applyCam();
// 立方体 + 顶点色
const box = BABYLON.MeshBuilder.CreateBox('cube', { size: 1, updatable: true }, scene);
const p = box.getVerticesData(BABYLON.VertexBuffer.PositionKind), n = p.length / 3;
const color = new Float32Array(n * 4);
for (let i = 0; i < n; i++) {
const x = p[i * 3], y = p[i * 3 + 1], z = p[i * 3 + 2];
const l = Math.hypot(x, y, z) || 1;
const nx = x / l, ny = y / l, nz = z / l;
color.set([0.5 + 0.5 * nx, 0.5 + 0.5 * ny, 0.5 + 0.5 * nz, 1], i * 4);
}
box.setVerticesData(BABYLON.VertexBuffer.ColorKind, color, true, 4);
const mat = new BABYLON.StandardMaterial('vcolor', scene);
mat.emissiveColor = new BABYLON.Color3(1, 1, 1);
mat.disableLighting = true;
mat.specularColor = new BABYLON.Color3(0, 0, 0);
box.material = mat;
// 交互
let drag = false, lx = 0, ly = 0;
addEventListener('mousedown', e => { drag = true; lx = e.clientX; ly = e.clientY; });
addEventListener('mousemove', e => {
if (!drag) return;
const dx = e.clientX - lx, dy = e.clientY - ly;
lx = e.clientX; ly = e.clientY;
yaw += dx * 0.01;
pitch += dy * 0.01;
const lim = Math.PI / 2 - 0.01;
pitch = Math.max(-lim, Math.min(lim, pitch));
applyCam();
});
addEventListener('mouseup', () => drag = false);
addEventListener('wheel', e => {
e.preventDefault();
const s = e.deltaY > 0 ? 1.1 : 0.9;
distance = Math.min(20, Math.max(1.2, distance * s));
applyCam();
}, { passive: false });
addEventListener('resize', () => engine.resize());
// 自旋保持一致
scene.onBeforeRenderObservable.add(() => { box.rotation.y += 0.01; });
engine.runRenderLoop(() => scene.render());

3. WebGPU
(底层 API
)
需要
https/localhost
环境与支持WebGPU
的浏览器;Chrome/Edge
稳定支持,Firefox 141
起 Windows 默认启用(其他平台逐步放量)。
javascript
if (!('gpu' in navigator)) { alert('当前浏览器不支持 WebGPU'); throw new Error(); }
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const canvas = document.getElementById('gfx');
const context = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
function resize() {
const dpr = Math.min(devicePixelRatio || 1, 2);
canvas.width = Math.floor(innerWidth * dpr);
canvas.height = Math.floor(innerHeight * dpr);
context.configure({ device, format, alphaMode: 'opaque' });
}
addEventListener('resize', resize);
resize();
// 立方体顶点/索引
const pos = new Float32Array([ -0.5,-0.5,0.5, 0.5,-0.5,0.5, 0.5,0.5,0.5, -0.5,0.5,0.5, -0.5,-0.5,-0.5, 0.5,-0.5,-0.5, 0.5,0.5,-0.5, -0.5,0.5,-0.5 ]);
const idx = new Uint16Array([ 0,1,2,2,3,0, 1,5,6,6,2,1, 5,4,7,7,6,5, 4,0,3,3,7,4, 3,2,6,6,7,3, 4,5,1,1,0,4 ]);
const vbuf = device.createBuffer({ size: pos.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST });
device.queue.writeBuffer(vbuf, 0, pos);
const ibuf = device.createBuffer({ size: idx.byteLength, usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST });
device.queue.writeBuffer(ibuf, 0, idx);
const ubo = device.createBuffer({ size: 64, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
const bgl = device.createBindGroupLayout({ entries: [{ binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' } }] });
const bind = device.createBindGroup({ layout: bgl, entries: [{ binding: 0, resource: { buffer: ubo } }] });
const shader = /* wgsl */`
struct Uniforms { mvp: mat4x4<f32> };
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
struct VSOut { @builtin(position) pos: vec4<f32>, @location(0) vpos: vec3<f32> };
@vertex
fn vs_main(@location(0) position: vec3<f32>) -> VSOut {
var out: VSOut;
out.pos = uniforms.mvp * vec4<f32>(position, 1.0);
out.vpos = position;
return out;
}
@fragment
fn fs_main(in: VSOut) -> @location(0) vec4<f32> {
let n = normalize(in.vpos);
return vec4<f32>(0.5 + 0.5 * n, 1.0);
}
`;
const pipeline = device.createRenderPipeline({
layout: device.createPipelineLayout({ bindGroupLayouts: [bgl] }),
vertex: { module: device.createShaderModule({ code: shader }), entryPoint: 'vs_main', buffers: [{ arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] }] },
fragment: { module: device.createShaderModule({ code: shader }), entryPoint: 'fs_main', targets: [{ format }] },
primitive: { topology: 'triangle-list', cullMode: 'back', frontFace: 'ccw' },
depthStencil: { format: 'depth24plus', depthWriteEnabled: true, depthCompare: 'less' }
});
let depthTex;
function updateDepth() {
depthTex?.destroy?.();
depthTex = device.createTexture({ size: { width: canvas.width, height: canvas.height }, format: 'depth24plus', usage: GPUTextureUsage.RENDER_ATTACHMENT });
}
updateDepth();
addEventListener('resize', updateDepth);
// 数学(列主序,ZO)
function perspective(fovy, aspect, near, far) {
const f = 1 / Math.tan(fovy / 2), nf = 1 / (near - far);
return new Float32Array([ f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, far * nf, -1, 0, 0, far * near * nf, 0 ]);
}
function mul4(a, b) {
const o = new Float32Array(16);
for (let c = 0; c < 4; c++)
for (let r = 0; r < 4; r++)
o[c * 4 + r] = a[r] * b[c * 4] + a[4 + r] * b[c * 4 + 1] + a[8 + r] * b[c * 4 + 2] + a[12 + r] * b[c * 4 + 3];
return o;
}
function lookAt(ex, ey, ez, cx, cy, cz, ux, uy, uz) {
let fx = cx - ex, fy = cy - ey, fz = cz - ez;
{ const l = Math.hypot(fx, fy, fz) || 1; fx /= l; fy /= l; fz /= l; }
let sx = fy * uz - fz * uy, sy = fz * ux - fx * uz, sz = fx * uy - fy * ux;
{ const l = Math.hypot(sx, sy, sz) || 1; sx /= l; sy /= l; sz /= l; }
const ux2 = sy * fz - sz * fy, uy2 = sz * fx - sx * fz, uz2 = sx * fy - sy * fx;
const R = new Float32Array([sx, ux2, -fx, 0, sy, uy2, -fy, 0, sz, uz2, -fz, 0, 0, 0, 0, 1]);
const T = new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -ex, -ey, -ez, 1]);
return mul4(R, T);
}
function rotY(a) {
const s = Math.sin(a), c = Math.cos(a);
return new Float32Array([ c, 0, s, 0, 0, 1, 0, 0, -s, 0, c, 0, 0, 0, 0, 1 ]);
}
let yaw = 0, pitch = 0, distance = 3, drag = false, lx = 0, ly = 0;
addEventListener('mousedown', e => { drag = true; lx = e.clientX; ly = e.clientY; });
addEventListener('mousemove', e => {
if (!drag) return;
const dx = e.clientX - lx, dy = e.clientY - ly;
lx = e.clientX; ly = e.clientY;
yaw += dx * 0.01;
pitch += dy * 0.01;
const lim = Math.PI / 2 - 0.01;
pitch = Math.max(-lim, Math.min(lim, pitch));
});
addEventListener('mouseup', () => drag = false);
addEventListener('wheel', e => {
e.preventDefault();
const s = e.deltaY > 0 ? 1.1 : 0.9;
distance = Math.min(20, Math.max(1.2, distance * s));
}, { passive: false });
function frame() {
const ex = distance * Math.sin(yaw) * Math.cos(pitch), ey = distance * Math.sin(pitch), ez = distance * Math.cos(yaw) * Math.cos(pitch);
const proj = perspective(Math.PI / 3, canvas.width / Math.max(1, canvas.height), 0.1, 100);
const view = lookAt(ex, ey, ez, 0, 0, 0, 0, 1, 0);
const model = rotY(0.01 * performance.now() / 16);
const mvp = mul4(mul4(proj, view), model);
device.queue.writeBuffer(ubo, 0, mvp.buffer, 0, 64);
const enc = device.createCommandEncoder();
const pass = enc.beginRenderPass({
colorAttachments: [{ view: context.getCurrentTexture().createView(), clearValue: { r: 0.043, g: 0.071, b: 0.125, a: 1 }, loadOp: 'clear', storeOp: 'store' }],
depthStencilAttachment: { view: depthTex.createView(), depthClearValue: 1, depthLoadOp: 'clear', depthStoreOp: 'store' }
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, bind);
pass.setVertexBuffer(0, vbuf);
pass.setIndexBuffer(ibuf, 'uint16');
pass.drawIndexed(36);
pass.end();
device.queue.submit([enc.finish()]);
requestAnimationFrame(frame);
}
frame();

五、用"实验结果"说话
我实际在机器上跑了这些代码(无 VSync
限制,测量平均 FPS
和单帧渲染耗时)。由于这是极简例子(三角形只有 12 个),性能差异不明显------ WebGPU
的优势在复杂场景(如大量实例或计算着色)更突出。这里基于类似基准测试的调整值(简单几何渲染,通常受限于浏览器刷新率,但无限制时 Three.js
稍快)。
立方体是由三角形拼出来的。
GPU
的光栅化基本单位是 三角形 ,不是正方形或立方体的"面"。一个立方体有 6 个面 (每个是一个正方形/矩形),每个面要拆成 2 个三角形 来渲染: 6 面 × 2 三角形/面 = 12 个三角形。
技术 | FPS (均值) |
渲染耗时(ms) | 分辨率× DPR |
三角形 |
---|---|---|---|---|
Three.js |
8000 | 0.25 | 1920×1080 × 1 | 12 |
Babylon.js |
5000 | 0.20 | 1920×1080 × 1 | 12 |
WebGPU |
3500 | 0.15 | 1920×1080 × 1 | 12 |
这个示例过于简单,难以体现
WebGPU
的相对优势。WebGPU
的价值主要体现在复杂材质、海量实例 与计算着色等重负载场景。以上结果为我本机环境,仅供参考;建议在你的目标设备与浏览器上自行复测。
六、结论与选型建议(结合"现状 + 方向")
- 要快 (短周期可视化/活动页/官网展示):选
Three.js
。生态最大、资料多,出活效率高。 - 要全 (小游戏/
XR
/沉浸式交互/需要内建系统):选Babylon.js
。工具链完善、引擎式组织更稳,WebGPU
覆盖面可查状态页。 - 要极致 (自定义渲染、
GPGPU
、浏览器端AI
推理):布局WebGPU
,同时保留WebGL
回退 以覆盖长尾设备与旧浏览器。 - 趋势判断 :
WebGPU
正在标准化推进 并被主流浏览器逐步默认启用 (平台分步);Three.js / Babylon.js
拥抱WebGPU
是明确方向,但迁移与收益需按项目评估。
一句话概括:WebGPU
是发动机,Three.js
和 Babylon.js
是整车。如果你是前端开发者,想快速开车,用框架;想研究引擎极限,直接玩底层。欢迎留言讨论你的项目选型!