three.js中如何等待<canvas>完成渲染更新以获取真实FPS?
在requestAnimationFrame()循环中调用renderer.render()时,其触发速度往往快于<canvas>元素更新图像的速度,导致真实FPS测量结果不准确。
比如在卡顿的Three.js场景中,若在页面HTML元素显示帧号,用高帧率(约120Hz)录制屏幕会发现:画布仅显示2张不同图像,但帧号从1340跳到1343,实际跳过了2帧,这会虚高FPS测量值——本质是requestAnimationFrame()的触发速度快于浏览器重绘<canvas>及页面其他元素的速度。
我已尝试多种方案,但效果不佳:
- 将
<canvas>连接到<video>元素,利用video.requestVideoFrameCallback(callback):这是唯一能阻止跳帧的方法,但会引入更严重的卡顿,得不偿失。 - 使用MessageChannel方案:无效果。
- 使用
requestAnimationFrame(() => requestAnimationFrame(callback)):无效果。
因此,是否存在可靠的方法,阻塞requestAnimationFrame()的每次迭代,让画布及页面所有元素完成重绘?以此获取屏幕上实际可见帧的真实FPS测量值。
补充说明:修改场景中InstancedMesh的实例数量(如const mesh = new THREE.InstancedMesh(geometry, material, 100);)可让场景卡顿,高帧率录制屏幕并快速旋转相机时,能看到帧号跳变(截图中可见1022和1029帧被跳过)。
要准确测量屏幕上实际可见帧的FPS,核心是等待浏览器完成真实的像素绘制后再触发下一帧渲染,以下是两种可靠方案:
1. 使用requestPostAnimationFrame(Chrome 104+)
这是浏览器原生提供的API,会在浏览器完成当前帧的像素绘制后触发回调,完美匹配“等待重绘完成”的需求。
修改你的渲染循环:
function render() { // 执行场景更新、渲染操作 renderer.render(scene, camera); // 更新帧号等UI元素 updateFrameCounter(); // 等待浏览器完成重绘后再触发下一帧 requestPostAnimationFrame(render); } // 启动循环 requestPostAnimationFrame(render);
这个API直接绑定浏览器的绘制流程,能确保每一次渲染都对应屏幕上的一帧可见画面,不会出现帧号跳变的情况。
2. 基于PerformanceObserver监听绘制标记(兼容更广)
如果需要兼容不支持requestPostAnimationFrame的浏览器,可以通过监听浏览器的绘制性能标记来判断重绘完成:
let isRendering = false; const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { if (entry.name === 'mark-render-end' && !isRendering) { isRendering = true; requestAnimationFrame(render); } } }); observer.observe({ entryTypes: ['mark'] }); function render() { isRendering = false; // 场景更新与渲染 renderer.render(scene, camera); updateFrameCounter(); // 标记渲染结束,等待PerformanceObserver监听绘制完成 performance.mark('mark-render-end'); } // 启动循环 requestAnimationFrame(render);
原理是在渲染完成后打一个性能标记,当浏览器完成绘制后,PerformanceObserver会捕获到相关的绘制事件,此时再触发下一帧渲染,确保每一次渲染都对应一次真实的屏幕更新。
关键注意点
- 不要尝试用同步阻塞的方式(如
setTimeout、while循环)来等待重绘,这会导致浏览器主线程卡死,反而加剧卡顿。 - 上述方案都是异步等待浏览器的绘制完成信号,不会阻塞主线程,同时能保证帧号与实际可见帧一一对应,从而得到准确的FPS数值。
内容的提问来源于stack exchange,提问作者user3163495

