Three.js场景在浏览器标签页闲置后冻结,角色动画停止运行
这个问题我之前做Three.js互动项目时也踩过坑!控制台没报错但场景假死,核心原因其实是浏览器的后台资源节流机制在搞鬼,给你拆解下:
问题根源
浏览器为了节省CPU和GPU资源,当标签页处于后台时,会把requestAnimationFrame的触发频率从平时的60次/秒,直接降到1次/秒甚至更低。看你Time.js里的delta计算逻辑:
this.delta = (currentTime - this.current) / 1000
如果标签页后台待个几分钟,切回前台的第一次tick计算时,currentTime - this.current会是一个超级大的数值——比如后台5分钟,这个差值就是300000毫秒,换算成delta就是300秒!
当这么夸张的deltaTime传给Three.js的AnimationMixer.update()时,mixer会尝试一次性处理这几百秒的动画:要么直接把动画跳到最后一帧(之后没有新的动画触发,看起来就像冻结),要么瞬间处理海量动画数据导致浏览器主线程阻塞,表现为场景完全假死,而且控制台不会抛出语法错误,因为这属于逻辑异常。
修复方案(两种思路,可单独用也可组合)
方案1:给deltaTime加上限(最直接的快速修复)
修改Time.js里的delta计算逻辑,强制给它设置一个最大值,比如限制为0.1秒(相当于10帧/秒的上限,既不影响正常动画流畅度,又能避免后台切回时的超大delta):
tick() { const currentTime = Date.now() // 限制delta最大值为0.1秒,彻底避免后台切回时的异常大值 this.delta = Math.min((currentTime - this.current) / 1000, 0.1) this.current = currentTime this.elapsed = (this.current - this.start) / 1000 this.trigger('tick') window.requestAnimationFrame(() => { this.tick() }) }
如果不想修改全局的Time类,也可以在每个调用mixer.update的地方单独做限制,比如Box.js和TamagotchiController.js:
update(deltaTime) { if (this.mixer) { const safeDelta = Math.min(deltaTime, 0.1) this.mixer.update(safeDelta) } }
方案2:监听页面可见性,切回时重置时间
给Time.js加上页面可见性监听,当页面从后台切回前台时,直接重置current时间,从根源避免超大delta的计算:
constructor() { super() // Setup this.start = Date.now() this.current = this.start this.elapsed = 0 this.delta = 0.016 // 监听页面前后台切换事件 document.addEventListener('visibilitychange', () => { if (!document.hidden) { // 切回前台时,把current重置为当前时间,避免计算出异常大的delta this.current = Date.now() } }) window.requestAnimationFrame(() => { this.tick() }) }
个人建议
最好把两个方案组合起来用:既限制delta的最大值,又监听页面可见性重置时间,双重保险下,不管标签页后台待多久,切回前台时动画都能正常运行。我之前用这个组合方案修复过类似的Tamagotchi风格项目,后台挂半小时再切回,动画依然丝滑,你可以试试!
备注:内容来源于stack exchange,提问作者cconsta1

