Code前端首页关于Code前端联系我们

Vue3项目里用ThreeJS做3D场景怎么才能不卡加载不闪屏不占内存?

terry 17小时前 阅读数 257 #Vue

最近整理了后台粉丝和交流群里的高频问题,发现刚接触Vue3+ThreeJS组合的朋友,80%都踩过这三个雷:第一次打开3D页面要等好几秒才出模型,切换路由回来模型要么全白要么闪一下,还有页面挂久了手机发烫电脑风扇狂转——这其实不是组合本身的问题,是我们没吃透两个工具的“脾气”,没做适配和优化,今天就把踩过的坑、试了几十种方案才总结出来的干货全抖出来。

先解决第一个问题:模型加载慢到怀疑人生

新手第一次用,大概率是直接下载了一个几MB甚至几十MB的GLB/GLTF文件,然后在Vue3的setup里直接调用GLTFLoader.load(),等半天不说,网络慢的话还可能超时,那怎么才能把加载速度提上来? 首先得搞明白,ThreeJS官方其实早就有轻量化模型的工具链,只是很多人不知道怎么串联起来用,我的流程是:先用Blender做模型清理,去掉多余的UV图层、隐藏的顶点/面、重复的材质,把PBR材质的纹理图压缩成WebP或KTX2格式——这里有个细节,纹理图的尺寸最好是2的幂次方,比如256×256、512×512,这样ThreeJS渲染时能直接加载进GPU纹理缓存,不会二次裁剪缩放浪费资源,清理和压缩之后,再用官方的gltf-pipeline工具把模型转为二进制的GLB,还可以开个Draco压缩参数——注意,Draco压缩虽然会让模型小70%-90%,但解压缩需要CPU计算,要是在低端手机上用,别开最高级别,我一般用medium,模型和纹理加起来控制在500KB以内,加载速度基本能控制在1秒左右(5G和WiFi6下)。 然后是Vue3这边的加载时机问题,别在setup或者onMounted里一上来就同步加载大模型,那样会阻塞Vue的DOM渲染,导致整个页面白屏加载,正确的做法是用异步组件包裹ThreeJS的3D画布组件,或者把模型加载逻辑放在DOM完全渲染完成之后的setTimeout里(时间设个100-200ms就行,不用太长),还可以配合HTML5的预加载纹理图,ThreeJS拿到纹理图的时候直接用缓存好的,不用重新请求网络,要是有多个小模型,可以分批加载,先加载主场景里的核心模型,加载完成显示出来之后,再用后台线程(Web Worker)加载装饰性的小模型,这样用户能第一时间看到东西,体验感会好很多。

再解决第二个问题:切换Vue3路由回来模型全白/闪屏

这个问题是组合踩雷最多的,核心原因是ThreeJS的3D上下文(WebGLRenderingContext)是绑定在canvas元素上的,而Vue3的路由切换(尤其是keep-alive的模式不对或者没开)会销毁/重建DOM元素,同时ThreeJS里的requestAnimationFrame循环、场景、相机、灯光这些东西还没销毁或者没正确重新绑定,就会出问题。 那怎么彻底解决?得分两种情况处理:一种是不需要缓存3D场景的页面(比如进入商品详情页只看一次3D模型,回来首页不需要恢复之前的视角),另一种是需要缓存的页面(比如一个虚拟展厅,用户逛到一半去看其他展厅的介绍,回来要接着逛原来的位置)。 先讲不需要缓存的情况,最简单也最稳妥:在Vue3的onBeforeUnmount生命周期里,一定要手动销毁所有ThreeJS的资源,顺序还不能错——先把requestAnimationFrame的循环取消掉(记得把循环ID存到ref或者reactive里,方便取消),然后销毁场景里的所有几何体、材质、纹理,接着销毁相机、灯光、渲染器,最后清除canvas元素的父容器,这里有个容易被忽略的点,就是纹理和材质的销毁,不能只调用dispose(),如果纹理是用loadTexture()加载的,还得调用texture.image.src = ''来释放浏览器的图片内存;如果材质有纹理数组,得遍历数组一个个dispose。 再讲需要缓存的情况,这时候就要用Vue3的keep-alive,但不能直接套,因为keep-alive默认会缓存组件的状态,但ThreeJS的canvas元素和上下文还是会被销毁重建,所以得用Vue3的把3D画布组件移到body根节点下,这样路由切换的时候canvas不会被销毁,然后在组件的onActivated和onDeactivated生命周期里控制requestAnimationFrame循环的开启和暂停——onActivated的时候开启循环,恢复渲染器的domElement.style.display为'block';onDeactivated的时候暂停循环,把display设为'none',不过用teleport也有个小问题,就是canvas的位置不好控制,得用fixed定位或者绝对定位配合z-index,还要在窗口大小变化的时候同步更新ThreeJS的渲染器尺寸和相机的宽高比,不然模型会变形。

最后解决第三个问题:页面挂久了内存飙升设备发烫

这个问题一般出现在长时间运行的3D项目里,比如虚拟售楼部、元宇宙展厅,核心原因是ThreeJS的资源没正确释放,或者requestAnimationFrame循环在不需要渲染的时候还在跑,占用了大量的CPU和GPU资源。 首先说资源释放,刚才讲路由切换的时候已经提过一部分,但还有动态加载/移除模型的情况,比如虚拟展厅里用户走到某个区域才加载某个展品,走了之后就移除,这时候一定要记得动态销毁移除的展品的所有几何体、材质、纹理,不能只从场景里remove()掉,remove()只是把模型从场景树里移除,不会释放GPU和CPU的内存,要是用了阴影,记得把不需要投射/接收阴影的模型的castShadow和receiveShadow设为false,阴影计算是很耗GPU的;如果场景里有很多重复的小模型(比如一片草地、一片森林),别一个个创建Mesh,要用InstancedMesh或者GLTF里的实例化,这样多个模型共用一个几何体和材质,能大大减少GPU的绘制调用次数。 然后是requestAnimationFrame循环的优化,默认情况下循环是每秒跑60次,不管场景有没有变化,这其实是浪费资源的,可以加个“脏标记”机制,只有当场景里有东西变化的时候(比如用户拖动了相机、点击了模型、模型做了动画),才把脏标记设为true,循环里只有检测到脏标记为true的时候才执行渲染,渲染完之后把脏标记设为false,要是用户的设备屏幕刷新率是120Hz,循环会自动跑到120Hz,这时候可以用ThreeJS的Clock或者setTimeout控制循环的帧率,比如锁在60Hz,减少设备的功耗,还有一个细节,就是用ThreeJS的WebGLRenderer的时候,可以设置preserveDrawingBuffer为false(默认就是false),如果设为true的话,渲染器会保存上一帧的画面,占用大量的GPU内存,除非你需要截图或者做视频录制,不然千万别开。

最后总结一下,Vue3+ThreeJS组合的优化核心其实就是两个:一是吃透两个工具的生命周期,正确绑定和销毁资源;二是减少不必要的资源占用和渲染调用,按照上面的方法优化之后,你的3D页面加载速度会快很多,不会闪屏,设备也不会发烫了,要是还有其他问题,欢迎在评论区留言。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门