1. 网页展示3D模型这件事,比想象中更贴近业务
1.1 需求场景拆解:客户要看的是“能动”的模型
先说个比较典型的场景。做工业设备、家装、电商或者教育类项目时,经常会遇到这样一个需求:产品经理或者甲方爸爸拿来一个做好的模型文件,希望在网页上展示出来,让访客能转一转、放大看看细节,而不是看一张死板的渲染图。
我最早接到这类需求时,第一反应是“这得用Three.js这种重型方案吧”,结果一查文档、一写代码,发现事情没那么夸张。网页展示3D模型本质上就三件事:准备一个浏览器能读懂的模型文件、写一段加载逻辑、让用户能交互查看。用对工具和流程,半天到一天就能跑通一个能看、能转、能缩放的基础版本。
这个需求的价值在于:它把“展示”从二维图片升级成了三维交互,用户能主动控制视角、观察细节。对于电商的商品详情、工业设备的说明书、建筑方案的汇报、教育课件的演示,这种交互体验带来的信息传递效率远高于静态图片。而且现在浏览器性能早就不是瓶颈了,WebGL 2.0 普及度极高,P100 显卡上跑几十万面片的模型也不在话下。
1.2 主流方案对比:Three.js、 、自研view3D组件
既然要展示3D模型,方案其实有好几条路。我梳理一下常见的做法,方便你根据场景选型。
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 原生Three.js | 功能最强,生态最成熟,可完全自定义 | 学习成本高,需要自己处理加载、灯光、相机、交互 | 大型项目、定制需求 |
<model-viewer>Web Component | 开箱即用,几行代码就能展示glTF/glb模型,自带AR支持 | 定制性弱,复杂交互难以实现 | 快速原型、商品展示、产品配置器 |
| 自研view3D组件 / 二次封装 | 结合Three.js能力和业务场景,输出一个可复用模块 | 需要维护,定制有门槛 | 中大型项目、多页面复用 |
我实际落地时用的是第三种思路:在Three.js之上封装一个轻量的view3D组件。这么做的好处是——既能拿到Three.js的全部渲染能力,又能把业务常用的功能(加载模型、自动旋转、热点标注、多模型切换)收敛成简单配置,后续在多个页面复用。
如果你只是临时要展示一个模型,<model-viewer>确实是最快的:引入一个js文件、写一行标签就能显示。但如果你的项目里有多个页面要用、要定制交互逻辑,封装一个组件是更省事的长期方案。
1.3 我为什么最终走了“封装view3D”这条路
很多人会问:既然Three.js都能做,为什么还要封装一层?我的理由是这么算的:
- 第一,业务里最常用的是“加载模型 + 控制视角 + 展示说明”,这些在三张页面里用同一个逻辑。如果每个页面都从头写一套Three.js初始化代码,重复劳动且容易出错。
- 第二,业务方不懂代码,他们只会提“这个模型能不能自己转”“能不能点某个零件看说明”“加载能不能快一点”。封装成组件后,这些需求就变成了配置项:
autoRotate: true、annotations: [...]、draco: true。 - 第三,后期维护只要改一个地方,所有页面同步生效。
所以,这篇文章的核心就是:基于Three.js封装一个view3D加载器,在网页上展示glTF/glb模型,并支持交互查看。整个流程从选型到实现,从踩坑到上线,我都按实际经历讲清楚。
2. 先搞懂view3D背后的渲染链路,才不会踩坑
2.1 模型格式:glTF/glb为什么是首选
做网页3D展示,第一个要决策的就是模型格式。这个问题不解决,后面全是坑。
当前Web端最推荐的是glTF(GL Transmission Format),以及它的二进制封装格式glb。它由Khronos Group维护,就是维护OpenGL和WebGL的那个组织,专为Web端传输和加载3D场景设计。
为什么glTF是首选?有几个核心原因:
- 它以JSON为核心描述场景结构,材质、贴图、动画、相机、灯光都能打包进去,信息完整。
- 支持
.bin二进制资源、Draco网格压缩、KHR纹理扩展,体积可以做到很小。 - 加载速度快,因为数据结构贴近GPU存储方式,底层解析效率高。
- 行业支持广泛:Blender、3ds Max、C4D、Maya等建模软件都能直接导出,Three.js有现成的加载器。
对比其他格式:OBJ只存几何数据,材质和动画支持弱;FBX虽然内容全但在Web端解析复杂、体积大;STL只有三角面片,没有颜色和材质信息。这些格式不是不能做,而是后续要处理的细节太多。所以我的建议是:从源头就把模型导成gltf/glb,省掉后面一整套转码流程。
2.2 从模型文件到屏幕像素,中间发生了什么
使用view3D加载模型时,核心流程可以拆成四个阶段:解析 → 上传GPU → 渲染循环 → 输出像素。
解析阶段,Three.js的GLTFLoader读取glTF/glb文件,把JSON描述的节点树(node hierarchy)、网格(mesh)、材质(material)、纹理(texture)转换成Three.js内部对象。这里有一个关键点:纹理和几何体数据不是直接上传GPU,而是先保存在内存里,等场景一切就绪后统一上传。
上传GPU阶段,Three.js会把几何体的顶点数据(位置、法线、UV、颜色)写入WebGLBuffer,把纹理图片转成WebGLTexture,然后编译Shader并链接Program。这个阶段是加载时的主要耗时来源,所以模型越大、纹理越多,首次加载越慢。
渲染循环阶段,就是浏览器每一帧调用requestAnimationFrame驱动的renderer.render(scene, camera)。这一步做了两件核心工作:一是根据相机视角做视锥裁剪(Frustum Culling),把屏幕外的物体剔除;二是按材质和深度排序,把可见物体一个个绘制出来。每一帧GPU都执行顶点着色器和片元着色器,最终把颜色值写入帧缓冲。
输出像素阶段,渲染结果通过Canvas展示给用户。如果你的容器是width: 50%这样的响应式布局,还需要在窗口尺寸变化时主动更新渲染器的尺寸和相机的纵横比,否则画面会拉伸变形。
这四个阶段里,普通开发者最容易感知到的是“加载慢”和“画面卡”,根因往往出在解析和渲染这一层。后面我会专门讲怎么优化。
2.3 相机、光照、材质:三个绕不开的基础概念
在你写view3D组件之前,有几个概念必须先在大脑里对齐,否则代码写起来容易“感觉对了但画面不对”。
- 相机(Camera):它定义了“从哪个视角看模型”。网页3D展示里最常用的是透视相机(PerspectiveCamera),它模拟人眼的近大远小效果。你需要设置
fov(视野角度)、aspect(宽高比)、near和far(近远裁剪面)。如果模型加载后看起来很小或很大,多半是相机位置和模型尺寸不匹配。 - 光照(Light):没有光,模型就是全黑的。Three.js里有很多种灯光,最基础的是环境光(AmbientLight)、方向光(DirectionalLight)和半球光(HemisphereLight)。环境光给整体一个基本亮度,方向光产生主阴影和立体感,半球光让暗部不那么死黑。我实际用的组合是:
环境光 + 方向光 + 半球光,覆盖大多数展示场景。 - 材质(Material):它决定了模型表面怎么和光相互作用。glTF材质的核心是PBR(Physically Based Rendering,基于物理的渲染),通过
baseColor(基础颜色)、metalness(金属度)、roughness(粗糙度)、normalTexture(法线贴图)等参数模拟真实世界材质。如果你在导出模型时没处理好贴图路径,加载出来材质会是一片灰白,这个坑后面详细说。
理解这三样东西,你就能预判很多问题:模型太暗加灯,模型太远调相机,材质不对查贴图。方向明确,排错快很多。
3. 手把手搭一个view3D展示页:从零到能转能点
3.1 环境准备:不需要构建工具也能跑
我说个让人松一口气的事实:用Three.js + view3D展示模型,不需要Vite、Webpack这些构建工具也能跑。
最简单的起步方式是用ES Module直接引用CDN文件。你只要建一个HTML文件,写一点JavaScript,就能把模型加载出来。这个方法特别适合快速验证模型文件是否正常、团队里非专业前端也能上手。
当然,如果项目本身已经有构建流程(比如Vue/React工程),更推荐用npm安装方式。但起步阶段、写博文分享、做Demo验证,直接上CDN是最不折腾的路径。
我这里用CDN + ES Module方式演示最小可运行案例,因为它的依赖最少、环境最干净,任何人复制粘贴就能看到效果。
3.2 加载第一个glb模型
先准备一个glb模型文件。如果你手头没有模型,可以去一些免费模型网站下载,或者用Blender随便建一个立方体导出成glb也行。重点在于验证流程。
下面这段是最小可运行的代码,先把模型显示出来:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>view3D 模型展示</title> <style> body { margin: 0; overflow: hidden; } #view3d-container { width: 100vw; height: 100vh; } </style> </head> <body> <div id="view3d-container"></div> <script type="importmap"> { "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js", "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/" } } </script> <script type="module"> import * as THREE from 'three'; import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'; import { OrbitControls } from 'three/addons/controls/OrbitControls.js'; // 创建场景、相机、渲染器 const container = document.getElementById('view3d-container'); const scene = new THREE.Scene(); scene.background = new THREE.Color(0xf5f5f5); const camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 0.1, 1000); camera.position.set(5, 5, 5); camera.lookAt(0, 0, 0); const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(container.clientWidth, container.clientHeight); renderer.setPixelRatio(window.devicePixelRatio); container.appendChild(renderer.domElement); // 添加灯光 const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2); directionalLight.position.set(5, 10, 7); scene.add(directionalLight); const hemisphereLight = new THREE.HemisphereLight(0xffffff, 0x444444, 0.8); scene.add(hemisphereLight); // 添加控制器 const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.dampingFactor = 0.08; // 加载模型 const loader = new GLTFLoader(); loader.load('/models/your-model.glb', (gltf) => { const model = gltf.scene; // 自动计算包围盒,调整模型位置和缩放 const box = new THREE.Box3().setFromObject(model); const center = box.getCenter(new THREE.Vector3()); model.position.sub(center); scene.add(model); // 根据模型尺寸调整相机距离 const size = box.getSize(new THREE.Vector3()).length(); camera.position.set(size, size, size); camera.lookAt(0, 0, 0); }); // 渲染循环 function animate() { requestAnimationFrame(animate); controls.update(); renderer.render(scene, camera); } animate(); // 窗口尺寸变化时适配 window.addEventListener('resize', () => { const width = container.clientWidth; const height = container.clientHeight; camera.aspect = width / height; camera.updateProjectionMatrix(); renderer.setSize(width, height); }); </script> </body> </html>如果你把这段代码保存成HTML文件,在本地起个HTTP服务(比如npx serve),把模型路径替换成实际文件,就能在浏览器里看到模型了。
有个细节要注意:模型加载到本地通过file://协议打开时,会出现跨域错误。因为浏览器默认不允许本地文件读其他本地文件。所以哪怕是最简单的Demo,也要用本地服务器方式运行。
3.3 让鼠标可以旋转缩放
模型能显示之后,最重要的一件事就是给用户交互能力。OrbitControls是Three.js官方提供的轨道控制器,用户可以通过鼠标左键旋转、滚轮缩放、右键平移。
上面代码里已经引入了OrbitControls,关键配置有这几个:
enableDamping: true:开启惯性阻尼,操作更顺滑,不会松手就立刻停。dampingFactor: 0.08:阻尼系数,数值越小惯性越大。autoRotate: true+autoRotateSpeed: 2:可以让模型像转盘一样自动旋转,适合商品展示没有人工操作时的默认状态。minDistance/maxDistance:限制相机和模型的距离范围,防止用户把相机怼到模型内部或者拉到太远看不到模型。maxPolarAngle:限制纵向旋转角度,避免用户把模型翻到底部看穿帮。
我实际开发里习惯给控制器的“探索边界”做一个约束,比如:
controls.minDistance = 2; controls.maxDistance = 20; controls.maxPolarAngle = Math.PI / 2;这些约束不是限制用户体验,而是保护交互的合理性。尤其是在“模型展柜”这种场景下,用户不应该钻到模型内部去,也不应该垂直翻转把场景倒过来看。
3.4 加一点进阶配置:自动旋转和加载进度条
交互能做之后,你会发现纯展示场景里,访客刚打开页面时没有明确操作目标,如果模型静静地不动,用户第一反应可能是“这是个图片”。所以做商品、展馆、介绍类页面时,几乎都会加自动旋转。
开启方式很简单:
controls.autoRotate = true; controls.autoRotateSpeed = 2; // 转速然后记得在用户开始操作(比如start事件)时暂停自动旋转,否则用户会在“转盘”上很难控制方向:
controls.addEventListener('start', () => { controls.autoRotate = false; });加载进度条也很有必要,尤其是模型超过10MB之后。用户体验上,一个明确进度条远比白屏等待让人安心。Three.js的GLTFLoader加载时支持onProgress回调:
loader.load(url, onLoad, (progress) => { const percent = (progress.loaded / progress.total) * 100; // 更新进度条UI });虽然onProgress在不同浏览器下不是百分百可靠(特别是跨域下载时total可能为0),但多数场景下足够用了。对超高可靠性的要求,可以考虑用fetch配合ReadableStream做,但普通业务没有这个必要。
3.5 整理出一个可复用的view3D组件代码
把上面的代码封装一下就变成了一个可复用的view3D组件。我用原生JavaScript类来写,方便你移植到任意框架:
class View3D { constructor(containerId, options = {}) { this.container = document.getElementById(containerId); this.options = Object.assign({ backgroundColor: 0xf5f5f5, autoRotate: false, autoRotateSpeed: 2, cameraPosition: [5, 5, 5], lights: { ambient: 0.6, directional: 1.2, hemisphere: 0.8 }, enableZoom: true, minDistance: 2, maxDistance: 50 }, options); this.scene = null; this.camera = null; this.renderer = null; this.controls = null; this.model = null; this.init(); } init() { // 与上面的初始化逻辑一致 // ... } loadModel(url, onProgress) { const loader = new GLTFLoader(); loader.load(url, (gltf) => { this.model = gltf.scene; // 自动适配相机距离 const box = new THREE.Box3().setFromObject(this.model); const size = box.getSize(new THREE.Vector3()).length(); this.camera.position.set(size * 1.5, size * 1.2, size * 1.5); this.camera.lookAt(0, 0, 0); const center = box.getCenter(new THREE.Vector3()); this.model.position.sub(center); this.scene.add(this.model); }, onProgress); } setAutoRotate(enabled) { this.controls.autoRotate = enabled; } dispose() { this.controls.dispose(); this.renderer.dispose(); if (this.model) { this.scene.remove(this.model); this.model.traverse((child) => { if (child.geometry) child.geometry.dispose(); if (child.material) { Object.values(child.material).forEach(value => { if (value && value.isTexture) value.dispose(); }); child.material.dispose(); } }); } } }这种封装方式让我在后续多页面使用时特别省心,每个页面只需要:
const viewer = new View3D('view3d-container', { autoRotate: true }); viewer.loadModel('/models/engine.glb');配置化之后,业务同学也能自己改参数,前端不用每次去动底层代码。
4. 实战中踩过的坑:加载慢、闪烁、纹理丢失和内存泄漏
4.1 模型加载慢:切片和Draco压缩,实测能省80%
很多人第一次展示模型,会遇到“模型加载了好半天才出来”的情况。排查下来发现:模型文件本身就有几十MB甚至上百MB。
这里有几个优化手段,按效果从大到小排列:
使用Draco压缩。Draco是Google推出的网格压缩算法,可以把几何数据压缩到原来的10%左右。Three.js加载时需要额外引入
DRACOLoader,并把压缩器文件部署到可访问路径。Blender导出gltf时勾选“Draco压缩”,选择压缩等级6~7,效果就很明显了。纹理图压缩。很多模型贴图是2048x2048甚至4096x4096的PNG,一张就十几MB。实际网页展示场景,把贴图缩到1024或512完全够看。用
sharp这类工具批量压缩,肉眼几乎无差别。模型减面。用Blender的“简化”修改器把不需要的高模精度降下来。比如一个工业零件展示需求,10万面片和50万面片在手机上看起来差不多,但性能差很多。
我遇到过最夸张的一个案例,原始模型glb有120MB,压缩后只有11MB,加载速度从12秒降到1.8秒。所以,模型优化永远是第一优先级。
4.2 透明PNG纹理变黑或白边,alphaTest是关键
这是个特别隐蔽的坑。模型里的一个零件使用了带透明通道的PNG贴图(比如镂空的栅格、树叶),加载到网页上发现透明区域变成了黑色或者有白边。
原因在于WebGL混色和深度测试的交互。Three.js里默认材质的transparent不是根据贴图自动开启的,需要手动设置,而且透明排序(transparent sort)是个世界级难题。
我的处理方式:在模型加载后遍历所有材质,检测是否有alpha贴图,然后做统一设置:
model.traverse((child) => { if (child.isMesh && child.material) { const materials = Array.isArray(child.material) ? child.material : [child.material]; materials.forEach((material) => { if (material.transparent) { material.depthWrite = false; // 防止透明区域遮挡其他物体 } if (material.alphaMap) { material.transparent = true; material.alphaTest = 0.5; // 半透明阈值 } }); } });alphaTest = 0.5的意思是:透明度低于0.5的像素直接丢弃,不做混合。这个设置能解决大多数“边缘白边”问题,比单纯开transparent更可控。
4.3 内存泄漏排查:切页面卡顿的隐形凶手
页面展示3D模型时,很多人会忽略内存问题。尤其是SPA应用里,从一个详情页切到另一个详情页,反复加载不同模型,内存占用会不断上涨,最终导致整站卡顿甚至崩溃。
原因在于:Three.js创建的场景、几何体、纹理、渲染器上下文不会自动释放。即使DOM节点被移除,WebGL资源依然驻留在GPU显存和JavaScript堆里。
我的组件里已经放了dispose方法,但实际工程里还要注意父子层级的释放顺序:先清场景、再释放控制器、最后调用renderer.dispose()。如果用了renderer.forceContextLoss(),清理更彻底,但要谨慎使用,因为它会释放整个WebGL上下文。
还有一个小技巧:用performance.memory配合DevTools Performance Monitor观察内存曲线。如果每次进入页面切换时,内存不回落,基本可以判定有泄漏。把组件生命周期里所有对象引用置空,养成习惯,能避免大多数泄漏。
4.4 安卓WebView和Safari兼容性:棱角分明的现实世界
开发完在Chrome里测试一切正常,一放到客户的安卓WebView或老iPad的Safari上,问题马上就来了。
最常见的问题和对应解法:
- WebGL不支持或支持度差:Android 5.0以下的系统很多不支持WebGL 2.0。检测要不要做?如果你的用户画像里有大量低端安卓设备,建议降级到WebGL 1.0渲染器,Three.js会自动处理大部分差异。
devicePixelRatio过高导致性能崩:部分安卓机的devicePixelRatio达到3甚至4,像素填充率压力巨大。实际开发中我常把像素比限制到2以内,画质提升已经超过大部分人眼可感知的极限,性能却能省一半。- iOS Safari的
100vh布局问题:这是老生常谈了,容器高度不要用100vh,Safari地址栏会遮挡。我一般用100dvh加position: fixed兜底,或者直接通过window.innerHeight设置容器高度。 - 旧设备不支持
import map:如果你像前面示例里用importmap,iOS 16.4以下不支持。工程化项目一般不会有这个问题,但纯CDN演示场景里要注意。稳妥做法是直接用type="module"配合完整URL导入,或者上构建工具。
浏览器兼容性问题没有银弹,务实的策略是:桌面端优先保证Chrome和Edge,移动端保证最新版Safari和Android WebView内核版本不低于Chrome 80。在这个底线之上,做渐进增强。
4.5 服务端MIME和跨域:一个返回头引发的血案
还有一个经常被忽略的点是服务端配置。glb文件在Nginx上默认没配置MIME类型,浏览器拿到后可能以application/octet-stream处理,某些浏览器下会导致加载失败或行为异常。
在Nginx配置里加上:
types { model/gltf-binary glb; }或者直接在location块里:
location ~* \.(glb|gltf)$ { add_header Content-Type model/gltf-binary; add_header Access-Control-Allow-Origin *; }另外,如果你把模型放在腾讯云COS、阿里云OSS这样的对象存储上,也要检查是否启用了“跨域设置”。否则浏览器发起跨域请求时,响应头没有Access-Control-Allow-Origin,模型照样加载不出来。
排查这类问题时,直接看浏览器Network面板里模型的响应状态。如果请求状态是(failed) net::ERR_FAILED或者CORS错误,优先检查响应头和服务端配置。
5. 进阶玩法:从展示到好用,产品级的细节打磨
5.1 多模型切换与懒加载:不是同时加载,而是按需加载
做电商或者产品选型场景时,页面上经常要展示多个配色或者多个型号的模型。新手常见的做法是把所有模型一次性全部加载进来,结果页面打开要等很久。
更务实的做法是懒加载:先展示第一个模型,用户切换时再加载对应的模型,加载期间显示进度条或占位图。如果网络条件好,可以在切换前利用空闲时间预加载。
我常用的实现思路是:
- 页面加载时,只加载默认模型。
- 用户点击型号A,先判断该模型是否加载过;加载过就直接显示,没加载过就异步加载。
- 加载期间用一个轻提示“模型加载中……”,加载成功后把之前的模型移除并销毁资源。
- 浏览器
requestIdleCallback空闲时,按预估优先级把可能用到的模型提前拉到缓存里。
这个方案实测下来,首屏加载时间能减少60%以上,用户切换时的等待感知也低很多。
5.2 热点标注与点击交互:让模型“讲故事”
有的业务场景里,光能旋转查看还不够。比如设备说明书里,用户需要点击水泵模型上的某个零件,看到零件名称和说明。这就是热点标注(Annotation)和点击拾取(Raycasting)。
Three.js里做点击拾取的核心是Raycaster:
const raycaster = new THREE.Raycaster(); const pointer = new THREE.Vector2(); function onClick(event) { // 将鼠标坐标转成NDC坐标 pointer.x = (event.clientX / window.innerWidth) * 2 - 1; pointer.y = -(event.clientY / window.innerHeight) * 2 + 1; raycaster.setFromCamera(pointer, camera); const intersects = raycaster.intersectObjects(model.children, true); if (intersects.length > 0) { const obj = intersects[0].object; // 根据obj.name关联业务数据,展示说明弹窗 showAnnotation(obj.name); } }关键点是:不同需要在建模时就约定好模型节点的命名规范。比如泵体叫body、叶轮叫impeller,前端才能把节点名映射到对应的产品资料。建模时命名规范,能省掉前端无数麻烦。
热点的实现则是在三维世界里用CSS2DRenderer把HTML标签叠加到模型对应位置,或者用Sprite精灵贴文字贴图。我个人更推荐CSS2DRenderer,因为可以完全用HTML/CSS控制样式,交互也更自然。
5.3 从建模软件到网页前端的模型优化流程
很多项目卡在“模型文件太大”这一关,其实问题根本不在于前端加载,而是建模阶段没有为Web展示做优化。
我建议团队内部统一一套DCC导出的规范流程:
- 单位统一:建模软件里统一用厘米或米,避免前端加载后模型尺寸忽大忽小。
- 面向Web减面:能少的面尽量少,保留外形轮廓即可。工业展示10万面片完全够用。
- 纹理从4096压到1024或512:电商首饰类的精细展示可以用2048,普通工业设备1024足够。
- 导出时勾选“仅选中对象”:清理掉场景里隐藏的参考物体。
- 导出为glb时,优先选择Draco网格压缩,压缩等级6左右。
- 避免使用导入导出附加插件时产生的超复杂材质节点:Blender的Cycles材质导出到glTF经常有问题,用Principled BSDF基础材质最稳妥。
这几点如果建模阶段做好了,前端几乎不用再做二次优化。我们团队现在把模型优化清单放进验收标准里,模型交付前先自检一遍,省掉了大量返工沟通。
5.4 性能预算参考表
最后给一个我实测过很多项目的性能预算参考表。这个表的意思不是绝对标准,但是一个很实用的体检基准:
| 项目 | 目标值 | 说明 |
|---|---|---|
| 首屏加载时间(桌面) | < 3秒 | 模型 + 依赖JS总体积控制在15MB内 |
| 首屏加载时间(移动端) | < 5秒 | 模型控制在8MB内,纹理控制在1024内 |
| 模型文件体积 | < 20MB | 超过20MB,必须做Draco压缩和减面 |
| 面片数量(移动端) | < 100万 | 超过100万,手机渲染会有明显卡顿 |
| 运行时帧率 | > 30 FPS | 用Chrome Performance Monitor持续观察 |
| 显存/内存占用 | 切换页面后可回落 | 用Performance观察GC后内存曲线 |
| Safari / Android WebView | 无报错 | 用真机,不要只看模拟器 |
这组数字是我做了十几个项目之后的经验值,不是Khronos官方的规范。如果你做的是高精机械零部件展示,可以适当放宽面片要求;如果是电商列表页里嵌入多个模型,则要把单模型体积压得更狠。
我个人的体会是:网页3D展示,永远是在“视觉质量”和“加载性能”之间做平衡。建模师喜欢高模高精贴图,前端想要轻快流畅,真正落地时两边要商量着来。你有了一份明确可量化的预算表,沟通成本会低很多。
最后分享一个小技巧:上线前用Chrome DevTools的性能面板录制10秒交互操作,观察主线程的脚本耗时和渲染耗时。如果Rendering时间超过8ms/帧,说明WebGL渲染负载偏高;如果Scripting时间占比过大,说明业务逻辑里有卡顿操作,可能需要做交互节流或Web Worker优化。这套排查流程我每次都会走一遍,能帮你在用户还没抱怨之前,就发现潜在的性能隐患。