2025 前端 3D 选型指南:Three.js、Babylon.js、WebGPU 深度对比
在前端世界里,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
import*asTHREEfrom"https://unpkg.com/[email protected]/build/three.module.js";const renderer =newTHREE.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 =newTHREE.Scene();const camera =newTHREE.PerspectiveCamera(60, innerWidth / innerHeight,0.1,100);let yaw =0, pitch =0, distance =3;functionapplyCam(){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 =newTHREE.BoxGeometry(1,1,1);const pos = geo.getAttribute('position');const col =newFloat32Array(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',newTHREE.BufferAttribute(col,3));const mesh =newTHREE.Mesh(geo,newTHREE.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();});(functiontick(){ mesh.rotation.y +=0.01; renderer.render(scene, camera);requestAnimationFrame(tick);})();
2. Babylon.js(引擎式结构)
const canvas = document.getElementById('c');const engine =newBABYLON.Engine(canvas,true,{ preserveDrawingBuffer:true, stencil:true});const scene =newBABYLON.Scene(engine); scene.clearColor =BABYLON.Color4.FromHexString('#0B1220FF');let yaw =0, pitch =0, distance =3;const camera =newBABYLON.FreeCamera('cam',newBABYLON.Vector3(0,0,3), scene);functionapplyCam(){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 =newFloat32Array(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 =newBABYLON.StandardMaterial('vcolor', scene); mat.emissiveColor =newBABYLON.Color3(1,1,1); mat.disableLighting =true; mat.specularColor =newBABYLON.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 默认启用(其他平台逐步放量)。
if(!('gpu'in navigator)){alert('当前浏览器不支持 WebGPU');thrownewError();}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();functionresize(){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 =newFloat32Array([-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 =newUint16Array([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;functionupdateDepth(){ depthTex?.destroy?.(); depthTex = device.createTexture({ size:{ width: canvas.width, height: canvas.height }, format:'depth24plus', usage: GPUTextureUsage.RENDER_ATTACHMENT});}updateDepth();addEventListener('resize', updateDepth);// 数学(列主序,ZO)functionperspective(fovy, aspect, near, far){const f =1/ Math.tan(fovy /2), nf =1/(near - far);returnnewFloat32Array([ f / aspect,0,0,0,0, f,0,0,0,0, far * nf,-1,0,0, far * near * nf,0]);}functionmul4(a, b){const o =newFloat32Array(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;}functionlookAt(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;constR=newFloat32Array([sx, ux2,-fx,0, sy, uy2,-fy,0, sz, uz2,-fz,0,0,0,0,1]);constT=newFloat32Array([1,0,0,0,0,1,0,0,0,0,1,0,-ex,-ey,-ez,1]);returnmul4(R,T);}functionrotY(a){const s = Math.sin(a), c = Math.cos(a);returnnewFloat32Array([ 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});functionframe(){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 是整车。如果你是前端开发者,想快速开车,用框架;想研究引擎极限,直接玩底层。欢迎留言讨论你的项目选型!