Three.js 的动画循环到底是谁在调?我在浏览器里对照了两种写法
Three.js 的动画循环到底是谁在调我在浏览器里对照了两种写法Three.js 示例里最常见的动画循环大概长这样每帧更新一点状态再调用requestAnimationFrame()预约下一帧。换成renderer.setAnimationLoop()后代码看起来像是把循环交给了渲染器。我想确认的是这会不会真的启动另一套时钟停止时该取消哪一层两个 renderer 同时开始会不会让回调按倍数增长我把 r170 放进同一个 Chrome 标签页分别测原生requestAnimationFrame、一个WebGLRenderer.setAnimationLoop()再让两个 renderer 同时运行。每组丢掉 12 次预热回调再记录 120 个间隔。前两组平均值都是 16.67 ms两个 renderer 各收到 120 次回调。这个结果没有证明setAnimationLoop()更快也没证明渲染器内部多了一只计时器。源码给出的答案反而简单在 WebGL 后端这个循环最后还是沿着浏览器的动画帧回调继续。先把“循环”说清楚动画循环至少有三个容易混在一起的东西谁请求下一帧、每帧执行了什么工作、以及何时停止。它们不是同一回事。浏览器把时间戳交给回调不代表场景已经更新回调里执行renderer.render()才会发起 Three.js 渲染回调里不做任何工作只能说明调度发生了不能代表 GPU 完成了一帧复杂画面。本次实验用 Three.js r170、Chrome WebGL2页面标题里显示 SwiftShader。计时只记录回调收到的时间戳差值不测 GPU 耗时也不把平均间隔叫作帧率性能。两种调度先后采样两个 renderer 的回调则同时采样。为了避免首次着色器准备、页面启动和第一次回调污染数据每一轮先忽略 12 次回调再取 120 个间隔。采样对象记录数平均间隔中位数P95最小—最大原生requestAnimationFrame12016.67 ms16.70 ms16.70 ms16.6—16.8 ms一个 renderer 的setAnimationLoop12016.67 ms16.70 ms16.80 ms16.5—16.8 ms两个 renderer 同时运行各自120 / 12016.67 / 16.67 ms16.70 / 16.70 ms16.80 / 16.80 ms16.6—16.8 ms这些差值集中在约 16.7 ms并不是 Three.js 保证了 60 FPS。当前浏览器环境给出的回调节奏接近 60 Hz显示器刷新率、标签页是否可见、电源策略和浏览器调度都会改变实际回调。这里的 P95 只描述本次标签页不能外推到另一台机器。特别是我没有在每个采样回调里持续调用renderer.render()所以这不是三种渲染负载的性能比较。原生 rAF 和单 renderer 组是先后测的每一组结束才开始下一组只有双 renderer 组把两个循环同时挂上。采样回调里只存浏览器给的时间戳既不更新 Mesh也不发 draw call。页面里的三个 TorusKnot 在进入计时前各画过一次随后画面保持静止。这样刻意把“谁调度下一次回调”和“渲染工作有多重”分开否则测出来的间隔会混入几何、材质、驱动和 GPU 的负载。WebGL 后端的setAnimationLoopThree.js WebGLRenderer 文档把setAnimationLoop(callback)作为动画循环入口并建议优先用它来兼容可能出现的 XR 场景。传入回调时开始循环传入null停止循环。它让调用方不必自己维护requestAnimationFrame的递归和取消句柄但 API 名称本身并不承诺更高刷新率。在 r170 的 WebGL 路径里实际负责调度的实现位于WebGLAnimation.js。start()会检查当前是否已经在运行以及回调是否存在运行后调用requestAnimationFrame(onAnimationFrame)帧回调再执行用户函数并预约下一次。stop()会用保存的 request id 调cancelAnimationFrame()。这就是我说“WebGL 后端仍是一条 rAF 链”的依据不是从 API 名字猜出来的。再看同一版本的WebGLRenderer.js构造渲染器时会创建一个WebGLAnimation实例。公开的setAnimationLoop()是把 callback 存到onAnimationFrameCallback再调用底层实例的start()或stop()它不是向回调列表里追加一项。因此同一个 renderer 上后一次调用会替换回调而不是再生一条链。实验中的两个 renderer 则各有自己的动画实例各自收到了 120 次通知。浏览器仍在同一个页面调度这些通知这不等于创建了两只独立硬件时钟。图里有个容易漏掉的时序WebGLAnimation.onAnimationFrame()是先调用外层帧处理器再无条件请求下一次 rAF。WebGLRenderer的外层处理器在回调被设为null后会变成空操作但不改变内层已经进入的这次函数。如果在用户回调内部同步调用setAnimationLoop(null)stop()取消的 request id 已经对应当前执行中的帧等回调返回内层仍会再排一帧。后续空回调还会继续被内层安排。这个结论来自 r170 源码的调用顺序本实验没有把“回调内部同步 stop”作为测量组。可以把递归关系缩成下面几行。它展示的是调度关系不代表这几行就完成了场景绘制functiononAnimationFrame(time,frame){animationLoop(time,frame);requestIdcontext.requestAnimationFrame(onAnimationFrame);}正常动画把更新和渲染放进回调renderer.setAnimationLoop((time){mesh.rotation.ytime*0.001;renderer.render(scene,camera);});第二段是应用侧的使用示例循环行为由setAnimationLoop调用不是实验脚本里用来统计间隔的回调。时间戳来自浏览器不宜把它当作固定步长。若每帧写position.x 0.01运动速度会随回调频率变化按时间戳算增量或使用固定步长更新才能把“每次回调走一步”改成“经过多少时间走多少路”。例如已有update()、scene、camera时可以用两次回调的时间差推进状态。停掉再启动时重新初始化上一帧时间避免把暂停的 250 ms 误算成角色突然移动的一大步functionstartLoop(){letpreviousTimenull;renderer.setAnimationLoop((time){constdeltaSecondspreviousTimenull?0:(time-previousTime)/1000;previousTimetime;update(deltaSeconds);renderer.render(scene,camera);});}functionstopLoop(){renderer.setAnimationLoop(null);}如果update()驱动的是物理模拟变步长也未必合适可以累计deltaSeconds按固定小步更新再把剩余时间用于插值显示。这个选择属于应用的时间模型换成setAnimationLoop()不会自动替你处理。为什么没有手写递归原生写法需要自己续帧functiontick(time){times.push(time);if(times.lengthWARMUP_FRAMESSAMPLE_COUNT1){requestAnimationFrame(tick);}else{resolve(times.slice(WARMUP_FRAMES,WARMUP_FRAMESSAMPLE_COUNT1));}}requestAnimationFrame(tick);实验的原生组确实沿用这种递归但渲染器组没有另造一个 rAF 包装而是把收集函数交给renderer.setAnimationLoop(tick)。达到采样数量后我把setAnimationLoop(null)放到setTimeout(..., 0)中停止。这样当前动画回调先返回WebGLAnimation 排入下一次 rAF计时器再取消待处理的 request id。它不是“等一帧再停”计时器任务通常在下一次浏览器帧回调前执行。本测试驱动脚本明确这样做不能把这一时序细节泛化成所有应用都应延迟停止。源码还处理了重复 start动画已经在运行时再次启动不会再建立一条并行递归链。这是“多次调用setAnimationLoop不等于多条循环”的关键。我的实验另设两个独立的WebGLRenderer各自设一个回调它们各自拿到 120 次回调测得的平均间隔都为 16.67 ms。这个结果描述两个独立循环都能收到浏览器回调不代表把两个 renderer 合并会省 GPU也不代表两个 renderer 只占一份绘制成本。停止之后帧计数有没有继续涨暂停测试先收 45 次 renderer 回调调用setAnimationLoop(null)再等待实际测得的 250.3 ms。为了确认标签页并没有一起停摆暂停期间另开一个独立的原生 rAF 探针它收到了 15 次回调而 renderer 的动画回调计数增加了 0。恢复后同一个 renderer 回调又收到 45 次。这里的 0 不再只是“暂停分支里没走到计数代码”因为独立探针证明浏览器的动画帧仍在继续。探针只自增计数并续约不触碰 WebGL 场景它用来确认标签页仍响应原生 rAF不是绘制耗时测试。两条预约链都在同一页面主线程上运行因此它也不能说明 GPU 或显示管线在这段时间做了什么。这也说明“暂停动画”和“让对象不再渲染”不是一回事。setAnimationLoop(null)停的是该 renderer 注册的循环回调对象仍留在场景里材质和几何体也没有被释放。如果页面隐藏时只是想避免动画更新可以停循环如果另一个循环仍在调用 renderer或者你在别处安排了 rAF停止这个 renderer 的循环不会替你取消那些工作。资源释放要单独处理不能把 stop 当成 dispose。恢复后为什么没有一个 250 ms 的大间隔因为脚本把暂停前后的时间戳分成两个数组统计时先分别求相邻差值再合并。每段 45 个时间戳各自产生 44 个间隔合起来是 88 个。这个处理回答的是“停止前后各自的回调间隔是什么”不是“用户经历了多久的暂停”。如果把两边数组直接拼起来再做差边界会产生一个约 250 ms 的值那是暂停时长不是动画循环抖动。实验代码怎么复跑浏览器页在experiments/animation-loops-browser.htmlPlaywright 驱动在experiments/animation-loops-browser.mjs每次运行会写experiments/animation-loops-browser-results.json。运行前在frontend目录启动静态服务器确保 Chrome 可执行文件路径和项目的 Playwright 依赖存在python3-mhttp.server8766--bind127.0.0.1nodeexperiments/animation-loops-browser.mjs统计器保留了每个间隔而不只写下均值也检查 r170 修订号、各组样本量、暂停探针帧数和恢复后的计数。仪表盘是浏览器运行后的页面截图原始精确值在 JSON 中。直方图把间隔分成12、12–20、20–30、≥30 ms四档本次 120 个间隔都落在 12–20 ms。两种循环的平均值四舍五入后相同P95 只有约 0.1 ms 差异在单次、同一标签页的测量里不足以支撑哪一种更快的结论。运行脚本会等页面把animationLoopResults写入window再断言修订号、120 个样本、双 renderer 的计数、暂停窗口和恢复帧数任何一项不符合预期都会中止截图与 JSON 输出。原始 JSON 保存全部间隔遇到均值看起来异常时可以直接检查每个时间戳差值而不是依赖图表的四舍五入结果。脚本固定了 macOS Chrome 可执行文件路径换系统复跑时需要改成当地浏览器路径。这组结果的边界测试使用 Chrome 的 SwiftShader而非指定型号的独立 GPU计时对象是 JavaScript 回调之间的间隔不是renderer.render()的 CPU 用时、GPU 执行时间或端到端显示延迟。双 renderer 组同时预约了两条循环但采样回调只记录时间没有在每个样本里画完整场景。页面中用于截图的三个 torus knot 只在采样前绘制了一次不能拿图像推断持续渲染负载。这是一轮浏览器采样不是不同机器、不同刷新率的重复基准。120 个相邻间隔能描出这次页面的回调分布但样本来自同一标签页、同一段运行时间不能当成 120 次独立设备测试。尤其表中两种方式的平均值只差不到 0.001 ms显示到两位小数后完全一样P95 相差约 0.1 ms也可能随浏览器调度轻微变化。把这个差值写成 API 性能优势就超出了数据。另外这次没有覆盖 WebXR session。Three.js 的推荐理由之一正是统一动画循环接口能够配合 XR我只验证普通 WebGLRenderer 路径。WebGPU renderer 的实现也不应从WebGLAnimation.js推断。官方文档和 r170 WebGL 源码支持的结论很窄普通 WebGLRenderer 的回调由 WebGLAnimation 管理而该实现使用浏览器 rAF停止传 null 会停止这条循环。实测时序与这个结构相符但回调均值接近 16.67 ms 本身并不能证明源码机制。项目里如果只是普通 WebGL 动画直接用renderer.setAnimationLoop()通常少维护一层 rAF 句柄停止时也有清晰入口。需要精确控制固定步长、多个更新阶段或多 renderer 协调时可以自己写调度但要明确每条循环何时启动、暂停、恢复和销毁。至于性能测实际渲染工作不要从“用了哪种循环 API”推断。

相关新闻

大型企业薪酬管理系统选型避坑指南:从发薪日惊心到稳定落地

大型企业薪酬管理系统选型避坑指南:从发薪日惊心到稳定落地

“这个标题起得一点都不夸张——我在多家企业见过同一个画面:发薪日上午HR还在用Excel反复核对,下午发现个税基数算错,财务拒绝付款,员工在钉钉群刷屏问工资;好不容易把数据理顺,银行代发接口又超时&#x…

2026/10/12 10:56:21 阅读更多 →
OpenCV 5 DNN模块实战:用Python构建神经网络计算机视觉解决方案

OpenCV 5 DNN模块实战:用Python构建神经网络计算机视觉解决方案

OpenCV 5的DNN模块,说真的,是我这两年用得最多的东西之一。别的不说,光是在本地跑深度学习推理这一点,就帮我省掉了一大堆远程调接口的麻烦事。今天这篇,就聊聊怎么用Python配合DNN模块,把神经网络计算机视…

2026/10/12 10:56:24 阅读更多 →
artcraft:手作与生成式AI融合的实体化创作工作流

artcraft:手作与生成式AI融合的实体化创作工作流

项目标题是“artcraft”,这个词本身由两个英文单词组合而成:art(艺术)与craft(工艺、手艺、精工制作)。它不是某个具体软件、平台或产品的官方名称,而是一个高度凝练的概念性复合词——在当下语…

2026/10/12 10:56:27 阅读更多 →

最新新闻

openclaw 用例翻译笔记:Event Guest Confirmation 的 Twilio 与 OpenAI 配置拆解

openclaw 用例翻译笔记:Event Guest Confirmation 的 Twilio 与 OpenAI 配置拆解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 15:04:46 阅读更多 →
FastReport批量打印实战:从循环直打到队列调度的稳定方案

FastReport批量打印实战:从循环直打到队列调度的稳定方案

1. 批量打印这件事,为什么值得单独拎出来说做过企业信息化的人都有一个共识:报表模块往往是项目里最不起眼、但上线后挨骂最多的部分。单张打印谁都会做,拖一个控件、绑一个数据源、点一下预览,完事。可一旦业务方甩过来一句“这批…

2026/10/12 15:04:46 阅读更多 →
C++实战:从CSV解析到疫情数据统计管线

C++实战:从CSV解析到疫情数据统计管线

1. 从一份CSV到一套分析管线:这个项目的真实定位很多人看到“用C分析疫情数据”这个标题,第一反应是:为什么不用Python?pandas几行代码就能搞定的事,何必用C折腾?这个问题我当初也问过自己。后来真正动手做…

2026/10/12 15:04:46 阅读更多 →
TDSC 2025区块链论文速读指南:从三看三问到笔记沉淀

TDSC 2025区块链论文速读指南:从三看三问到笔记沉淀

干过区块链方向研究的人应该都有这种体验:周末翻三篇论文,合上PDF之后能复述出主线的没几篇,更别提把方法复现出来了。尤其是TDSC这种CCF A类老牌期刊,随便一篇就是三十页起步,形式化模型、安全证明、系统实验一个不落…

2026/10/12 15:04:46 阅读更多 →
【Spring AI Alibaba】百炼工作流 + Multi-Agent 搭建电商智能导购:把 Base URL 改到 TaoToken 的完整配置

【Spring AI Alibaba】百炼工作流 + Multi-Agent 搭建电商智能导购:把 Base URL 改到 TaoToken 的完整配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 15:04:45 阅读更多 →
基于SSM+Vue的流浪动物领养助养系统毕业设计全流程解析

基于SSM+Vue的流浪动物领养助养系统毕业设计全流程解析

去年秋招失败后的那段时间,我每天从学校回出租屋都要绕路经过一家流浪动物救助站。救助站的负责人是个四十多岁的大姐,她给我看了三本厚得像字典的纸质登记册,里面密密麻麻写着编号、转账记录、领养人电话。她说他们最缺的不是爱心&#xff0…

2026/10/12 15:03:44 阅读更多 →

日新闻

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

复古胶片颗粒感噪点合成器:Canvas ImageData 像素高斯杂色注入算法

在数码相机、高清显示屏与现代矢量图形技术高度发达的今天,画面可以做到绝对的锐利、平滑与无瑕。然而,当一张秋日手账插画或拍立得照片过于“平整无瑕”时,往往会散发出一种冰冷生硬的“数码塑料感(Digital Plasticity&#xff0…

2026/10/12 0:00:59 阅读更多 →
活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

活字印刷古籍线装排版:Canvas 竖排文字与栏线自适应算法

在现代网页与移动端设计中,横排(Horizontal Layout)早已经成为了绝对的主流。然而,当我们翻开泛黄的线装古籍、宋版木刻诗集,或是欣赏一张茶道雅集的手写便签时,那种**自上而下纵向书写、自右向左逐列铺展&…

2026/10/12 0:00:59 阅读更多 →
周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

周日晚间的“精神松绑减震器”:无压力情绪倾倒箱与温和轻声陪伴

每到周日的晚上八点到十点,很多人心里都会悄悄亮起一盏警示灯。 在心理学上,这种现象有一个专门的称谓——“周日夜晚焦虑症(Sunday Scaries)”。明天又是周一,闹钟又要重新在七点响彻卧房;脑海里仿佛有一个…

2026/10/12 0:00:59 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/12 0:16:30 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/12 0:16:38 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/12 0:16:43 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 10:45:37 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 14:48:49 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/11 14:36:54 阅读更多 →