本节摘要:渲染循环是帧流水线的节拍器,由浏览器刷新信号驱动而非 while 死循环。本节讲清 runRenderLoop 的驱动机制、帧率与帧耗时的读数方法、掉帧的三种面孔与各自的归因方向,并给出按帧时间做自适应降质的常用手法。本节结束时,你能给自己的应用装上一块实时秒表。
两个新人同写渲染循环。A 用 while(true) 包住场景渲染,页面打开直接冻死;B 用引擎的 runRenderLoop,画面流畅。A 的直觉没错——确实要"不停地画",错在谁来喊停。渲染不是画得越快越好,而是要跟着显示器的节拍画:显示器每秒刷新 60 次左右,你在两次刷新之间多画的那几帧,用户根本看不见,只换来风扇起飞。
所以引擎的循环是"被驱动"的。浏览器在每次刷新前发出一个信号,引擎收到信号才执行一次完整渲染。下面的时序图就是这个约定的展开:
时序里藏着三个实用推论。其一,页面切到后台时浏览器停发信号,循环自动休眠,功耗随之下降——不需要你手动暂停。其二,帧与帧的间隔由显示器决定,不由你的代码决定,代码只能决定"这一帧的活儿干不干得完"。其三,一帧超时不会丢帧,而是"顶替"下一次刷新位,用户看到的实际帧率立刻减半——这正是掉帧如此显眼的原因。
谈帧率必须先有读数。引擎自带一组性能计数器,先把它显示出来:
// 每秒采样一次帧率,贴到页面角落或控制台 setInterval(() => { const fps = engine.getFps().toFixed(0); // 每秒帧数 const dt = engine.getDeltaTime(); // 上一帧耗时毫秒数 console.log(`帧率 ${fps},上一帧耗时 ${dt.toFixed(1)} 毫秒`); }, 1000);
帧率告诉你用户体感,帧耗时告诉你引擎内情,两个数要一起看。60 帧对应约 16.7 毫秒的预算,超过预算的那一帧,就是流水线上某个站点拖了后腿。把预算画成条形图,超标一眼可见:

第一种,CPU 超支。帧耗时曲线整体抬升,浏览器任务管理器里 GPU 并不忙。归因方向是流水线前六站:网格太多、每个网格单独成绘制调用、每帧新建大量对象。第二种,GPU 超支。CPU 有余量但帧率卡在某个值,降低画布分辨率后帧率明显回升——这是 GPU 着色超支的指纹。第三种,尖刺。平时 60 帧,偶发单帧几百毫秒,多半是纹理上传、着色器首次编译或垃圾回收,解法是把资源预热挪到加载阶段。
归因之后是处置。自适应降质是最常用的一招:按帧耗时动态调分辨率系数,卡了就降、松了就升:
// 一个极简的自适应分辨率:帧耗时连续超标则降档,连续富余则升档 let ratio = 1.0; // 硬件缩放系数,1 为原始分辨率 let slowStreak = 0; // 连续超帧计数 scene.onAfterRenderObservable.add(() => { const dt = engine.getDeltaTime(); if (dt > 20) { slowStreak++; if (slowStreak > 30 && ratio > 0.6) { // 连续 30 帧超标才动手,避免抖动 ratio = Math.max(0.6, ratio - 0.1); engine.setHardwareScalingLevel(1 / ratio); // 数值越大分辨率越低 slowStreak = 0; } } else { slowStreak = 0; } });
⚠️ 常见坑:在循环回调里做重型同步操作(大循环、同步网络请求)等于把整条流水线按住不放。重型任务挪到加载期或工作者线程,回调里只做轻量状态更新。
把三种面孔串成完整流程,走一遍真实案例。背景:一个虚拟展馆项目,开发机上稳定 60 帧,投到展会一体机后掉到 22 帧,现场只有十分钟。操作分三步。第一步读数:帧率 22、帧耗时约 45 毫秒,CPU 与 GPU 都有份但 CPU 更重。第二步归因:把镜头对准最密的展台,帧耗时几乎不变,说明不是可见集膨胀;把分辨率降到七成,帧率只回到 28,GPU 不是主犯。第三步查回调:翻代码发现每帧给 40 块展牌各做一次悬停拾取、还顺手克隆了一份材质——两处都塞在渲染回调里。结果:材质克隆挪到加载期、悬停拾取关闭,帧耗时回落到 14 毫秒,帧率稳定 60。解读:三步正好对应本节三块内容——秒表读数、面孔归因、回调减负;变式是若降分辨率后帧率显著回升,主犯在 GPU,处置走 5.1 节的像素压缩路径。两类处方完全不同,这就是"先归因再动手"的价值。
帧预算由目标帧率倒数而来,目标不是越高越好,而是按设备与场景用途定档:
| 目标帧率 | 帧预算 | 适用场景 | 主要代价 |
|---|---|---|---|
| 30 帧 | 约 33 毫秒 | 展厅一体机、长时低功耗运行 | 快速转镜发糊,体感一般 |
| 60 帧 | 约 16.7 毫秒 | 多数交互场景的默认目标 | 中端手机需节制材质与后期 |
| 90 帧以上 | 11 毫秒以内 | 头显渲染(第6章 WebXR 的硬门槛) | 每站预算近乎腰斩 |
还有两个细节常被漏掉。一是高刷屏幕:120 赫兹的显示器会把循环也拉到 120 次,帧预算直接砍到 8.3 毫秒,低配设备反而更卡——必要时给引擎设帧率上限,主动放弃用不满的高刷。二是读数要带样本量:单帧耗时没有意义,至少取几百帧的中位数与最差值一起看,偶发尖刺藏在最差值里。
第1章到此收束:你已经能通电、看图、掐表。下一章开始往流水线里放东西——先从被遍历的主体"场景图"讲起,看看一棵节点树如何撑起整个世界。