ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

RAF 渲染帧速通:从渲染帧到性能优化

RAF 渲染帧速通:从渲染帧到性能优化 最近整理前端性能优化相关的知识发现 requestAnimationFrameRAF这个 API 看着简单但真要讲清楚它和浏览器渲染、事件循环、性能优化的关系还是有不少细节。这篇是我自己的学习笔记把之前搞混的点梳理一下。01RAF 和 setTimeout 的区别一开始我以为 RAF 就是更精确的 setTimeout其实不是。setTimeout 是定时器任务表达的是至少经过指定时间后可以执行具体什么时候跑还要看事件循环和主线程空不空闲。比如 setTimeout(fn, 16) 并不保证 16ms 后一定执行如果主线程在忙回调会继续往后排而且执行时机和浏览器绘制不对齐。RAF 是浏览器在下一次绘制前统一调度的回调通常会在浏览器准备画下一帧之前跑。所以做动画、改 Canvas 内容、或者任何需要跟视觉更新对齐的操作用 RAF 更合适。另外一个容易踩的坑RAF 不是固定 16ms 一次。60Hz 屏幕一帧约 16.67ms120Hz 约 8.33msRAF 跟着实际刷新率走。所以动画不能写成每帧移动 2px不然 120Hz 屏幕上速度直接翻倍。图RAF 和 setTimeout 的调度时机对比还有一点页面切到后台后浏览器通常会暂停或大幅降低 RAF 的频率因为没必要继续画了。setTimeout 后台也会被节流但策略不一样。02RAF 在渲染流程里的位置浏览器一帧的流程大概是处理任务 → RAF → Style → Layout → Paint → Composite。当然不同浏览器细节有差异JS 里某些 DOM 读取也可能提前触发布局但大概可以这么理解。图RAF 在渲染流水线中的位置我之前误以为 RAF 是调用栈里的一个阶段其实不是。调用栈只是 JS 的执行结构RAF 是浏览器提供的调度机制在渲染机会到来时把回调排到主线程执行。还有个细节同一帧注册的多个 RAF 回调拿到的 timestamp 是一样的因为这个时间戳表示的是当前渲染帧的时间不是每个回调实际开始跑的时间。03为什么 RAF 适合做动画核心不是 RAF 让 JS 跑得更快而是让更新时机更合理。如果用 setTimeout 做动画回调可能刚好在一次绘制刚结束的时候触发那这次 DOM 修改就要等下一帧才能显示白白浪费一次更新机会。RAF 把更新安排在浏览器准备绘制的时候减少不必要的中间状态。但要注意RAF 不会自动解决卡顿。如果一个 RAF 回调跑了 30ms主线程还是被占住浏览器照样没法按时画下一帧。60Hz 的 16.67ms 不是全给 JS 的还要留时间给 Style、Layout、Paint、Composite真正能用在 JS 上的时间更少。04动画要基于时间不是帧数我之前写动画就是每帧固定移动 2px后来发现 120Hz 屏幕上速度直接翻倍。正确做法是用 RAF 给的 timestamp 算两帧之间过了多久然后按时间差算位移let lastTime nulllet position 0const speed 100 // px/sfunction step(timestamp) {’if (lastTime null) {’lastTime timestamp}const delta timestamp - lastTimelastTime timestampposition speed * delta / 1000box.style.transform translateX(${position}px)requestAnimationFrame(step)}requestAnimationFrame(step)这样不管是 60Hz、120Hz 还是 144Hz理论上一秒都移动约 100px。直接用 RAF 传进来的 timestamp 就行不用再单独调 performance.now()。05性能陷阱长任务和 Layout Thrashing之前我以为用了 RAF 就一定流畅后来发现不是。RAF 还是跑在主线程上如果回调里有 50ms 的长任务浏览器照样卡成狗。RAF 解决的是什么时候执行不是执行得多快。还有个坑写在 RAF 里也会 Layout Thrashing。比如改完样式立刻读 offsetWidth浏览器为了返回最新布局还是会被迫同步 Layout。优化原则还是老样子把 DOM 读和 DOM 写分开先批量读再统一改。CPU 密集的计算可以放到 Web Worker能拆分的活就分批做DOM 场景还要避免强制同步布局和大范围重渲染。06实战滚动优化和流式渲染scroll、pointermove 这些事件触发频率很高每次都直接更新 DOM 肯定不行。用 RAF 把多次事件合并到一帧只更新一次let scheduled falselet latestY 0window.addEventListener(‘scroll’, () {latestY window.scrollYif (scheduled) returnscheduled truerequestAnimationFrame(() {updateUI(latestY) scheduled false})})更有意思的是 React 流式渲染的场景。SSE 持续返回 Markdown token如果每收到一小段就 setContent网络层可能在很短时间内触发多次状态更新。我自己的做法是先把数据暂存到 Buffer每一帧最多提交一次图RAF 按帧合并 SSE chunk 的原理RAF 在这里的作用不是让 SSE 变快而是把网络事件触发频率和UI 渲染频率解耦。一帧内到了十个 chunk最终只触发一次 UI 更新。再配合 memo、稳定 Props、消息级状态隔离就不会因为一条消息变化导致整个列表重渲染。07高频面试追问速答QRAF 一定是 60FPS 吗不是。受刷新率、浏览器调度、主线程负载影响。60Hz 屏幕理想约 60 次/秒120Hz 可能到 120 次。动画按 timestamp 算进度就行。QRAF 属于宏任务还是微任务严格说不该这么归类。宏任务/微任务是事件循环的任务模型RAF 是浏览器渲染更新机制的一部分在合适的渲染机会执行。QRAF 能防抖节流吗可以实现按帧合并效果接近节流但不完全等于时间节流。它保证每帧最多一次不是每隔固定 100ms 一次。QRAF 和 requestIdleCallback 区别RAF 关注下一帧绘制前适合视觉更新requestIdleCallback 关注空闲时间适合低优先级、可延迟的任务。一个偏渲染一个偏空闲调度。QRAF 会内存泄漏吗本身不会但递归 RAF 如果组件卸载或任务结束时没取消回调会一直跑还持有组件引用记得在 cleanup 里 cancel。总结一下RAF 本质是浏览器面向渲染帧的调度 API回调在下一次绘制前执行跟随实际刷新率。它解决的是更新时机的问题不是让代码跑得更快——回调里有长任务、强制同步 Layout、大量 React Render一样掉帧。用的时候记得按时间算动画、记得清理、别拿它当后台任务跑。学习笔记如有错漏欢迎指正寻码札记
返回列表