JS requestAnimationFrame 底层实现

window.requestAnimationFrame()后浏览器会在下一次重绘前回调,前端在回调中再次调用该函数,则实现了与屏幕帧率一致的回调。

但发现 CPU 占用较高,内存也会缓慢增加,有些怀疑底层实现有问题。

底层实现

从 WebKit LocalDOMWindow::requestAnimationFrame入口查起。

  • MonitorManager 管理了一批 Monitor;
  • Monitor 管理了一批 displayID 相同的 MonitorClient;
  • Scheduler 继承 MonitorClient;

iOS 中 Monitor 是 DisplayRefreshMonitorIOS:

ini 复制代码
DisplayRefreshMonitorIOS::startNotificationMechanism
-m_handler = adoptNS([[WebDisplayLinkHandler alloc] initWithMonitor:this]);

@implementation WebDisplayLinkHandler
- (id)initWithMonitor:(DisplayRefreshMonitorIOS*)monitor {
    if (self = [super init]) {
        m_monitor = monitor;
        // Note that CADisplayLink retains its target (self), so a call to -invalidate is needed on teardown.
        m_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(handleDisplayLink:)];
        [m_displayLink addToRunLoop:WebThreadNSRunLoop() forMode:NSDefaultRunLoopMode];
        m_displayLink.preferredFramesPerSecond = DisplayLinkFramesPerSecond;
    }
    return self;
}
- (void)invalidate {
    [m_displayLink invalidate];
    m_displayLink = nullptr;
}

底层果然是 CADisplayLink 去实现重绘时机捕获,但这里 CADisplayLink 与 WebDisplayLinkHandler 形成了循环引用。继续排查:

scss 复制代码
    m_monitors.append(DisplayRefreshMonitorWrapper { WTFMove(monitor) });

    struct DisplayRefreshMonitorWrapper {
        ~DisplayRefreshMonitorWrapper() {
            if (monitor)
                monitor->stop();
        }
        RefPtr<DisplayRefreshMonitor> monitor;
    };

MonitorManager 添加 Monitor 时有个 Wrapper 类,而 Wrapper 在析构时会调用 Monitor 的 stop 函数,进而打破循环引用。

回调分发

分发起点:

ini 复制代码
void DisplayRefreshMonitorIOS::displayLinkCallbackFired() {
    displayLinkFired(m_currentUpdate);
    m_currentUpdate = m_currentUpdate.nextUpdate();
}

constexpr WebCore::FramesPerSecond DisplayLinkFramesPerSecond = 60;
m_currentUpdate = { 0, DisplayLinkFramesPerSecond };

currentUpdate 是个当前帧数与帧率的结构体:

arduino 复制代码
// Used to represent a given update. An value of { 3, 60 } indicates that this is the third update in a 1-second interval
// on a 60fps cadence. updateIndex will reset to zero every second, so { 59, 60 } is followed by { 0, 60 }.
struct DisplayUpdate {
    unsigned updateIndex { 0 };
    FramesPerSecond updatesPerSecond { 0 };
    DisplayUpdate nextUpdate() const  {
        return { (updateIndex + 1) % updatesPerSecond, updatesPerSecond };
    }
...

由此可见,requestAnimationFrame 的帧率上限最多 60。

后续的分发逻辑就没啥特殊的了。

结论

requestAnimationFrame 底层基于 CADisplayLink 实现,不存在内存泄露,帧率上限 60。

回调后涉及 JS 引擎对 Dom 树的刷新等,JS 是解释执行语言,每秒 60 次高频调用确实有些成本。而由于垃圾回收机制的滞后性,缓慢的内存增量可能仅是来不及回收。

JS 引擎层面的问题就不深究了,我们能做的就是尽量避免使用 requestAnimationFrame,若非要获取帧刷新回调,应该设计为按需触发循环,禁止时挂起。

相关推荐
烬羽1 小时前
后端返回的 JSON 字符串,浏览器怎么"看懂"的?——Ajax 全链路拆解
javascript
半个落月2 小时前
一个新手用 Bun + Axios 调通 DeepSeek API 的实践记录
javascript
不好听6132 小时前
深入理解链表:线性数据结构的另一面
javascript·数据结构
林希_Rachel_傻希希2 小时前
学React治好了我的焦虑症,1小时速通React 前20分钟。
前端·javascript·面试
小林ixn2 小时前
从 Ajax 到异步编程:JSON 序列化、Event Loop 与 XHR 请求完全解析
javascript
丷丩3 小时前
MapLibre GL JS第47课:添加动画图标
javascript·gis·动画·mapbox·maplibre
快乐的哈士奇4 小时前
【Next.js实战①】Gmail API 按柜号检索邮件:OAuth 双 Cookie 与搜索 Fallback
开发语言·javascript·ecmascript
云水一下4 小时前
Vue.js从零到精通系列(五):全局状态管理——Pinia 核心与实践
前端·javascript·vue.js
kmblack15 小时前
javascript计算年龄
开发语言·javascript·ecmascript
Dick5075 小时前
ROS2 多机器人通用 Driver 层复盘:BaseRobotDriver 到多平台 Mock 切换实现
前端·javascript·机器人