刷新率 FPS(Frames Per Second)
现实场景中网页内容的每一帧我们都称做一次渲染,当衡量一个浏览器页面的性能标准时,通常会说页面在 60FPS 的帧率下是否流畅,60FPS 换算一下就是每一帧要在 16.7ms (1000/60) 内完成渲染。因此当页面每秒绘制小于 60 帧时就是所谓的丢帧,人眼就能感受到页面出现卡顿。但是浏览器不会让页面偶尔丢帧而会选择降低帧率,例如选择30FPS。
补充:对于浏览器上下文不可见页面,帧率会降低到 4FPS 左右甚至更低。
单帧渲染流程 - 像素管道
通过 Google Developers 里的描述,用户拥有对帧性能最大控制权关键点为:
- JavaScript:经常会在实际场景中使用 JavaScript 来实现一些视觉变化的效果。
- 样式计算 Style :根据匹配选择器确定出最终 CSS 规则并应用到对应元素上。(选择器权重确定)
- 布局 Layout :知道元素对应的最终 CSS 规则后,计算出占据屏幕大小以及位置,创建只包含可见元素的布局树。(修改了元素大小、位置等会引发
重排 reflow) - 绘制 Paint :对每个
图层 Layer进行绘制,生成绘制列表,涉及绘出文本、颜色、图像、边框和阴影。(修改了元素的背景颜色、阴影等会引发重绘 repaint) - 合成 Composite :把每个图层的绘制列表按正确的顺序绘制到屏幕上。
浏览器事件循环与渲染机制
HTML5官方规范描述 EventLoop 的执行步骤 :
-
执行宏任务队列和微任务队列就不解释了。
-
进入
Update the rendering阶段,这里有个rendering opportunity概念,浏览上下文渲染会根据屏幕刷新率、页面性能、页面是否在后台来确定是否需要渲染。而且渲染间隔通常是固定的。因此每轮事件循环不一定会让浏览器进行渲染。一帧内可能会经历多轮事件循环,也就意味着会执行多个宏任务(Task)。
-
如果不需要渲染,以下步骤(只列举常用的)也不会运行了:
run the resize steps,触发resize事件;run the scroll steps,触发scroll事件;update animations,触发animation相关事件;run the fullscreen steps,执行requestFullscreen等 api;run the animation frame callbacks,执行 requestAnimationFrame 回调;run IntersectionObserver callbacks,图片懒加载经常使用;
-
重新渲染用户界面。
-
判断宏任务队列或者微任务队列是否为空,如果为空则执行
Idle空闲周期计算,判断是否需要执行requestIdleCallback的回调。
通过 Jake 演讲 EventLoop 中PPT里的图再来巩固一遍:
图中白色方块转一圈就代表一轮事件循环,两个开关是浏览器的行为控制。
- 左侧:任务队列;当有任务需要执行时,就加入
JS Stack中进行执行。 - 右侧:页面帧的渲染;浏览器会决定什么时候进行页面帧的渲染,需要渲染时才会打开开关,可以发现
rAF的回调会在页面渲染前执行。
rAF
rAF 也就是
requestAnimationFrame,MDN 中对这个接口描述是:要求浏览器在下次重绘之前调用指定的回调函数更新动画。
实际场景中用rAF来做对比的基本也是setTimeout、setInterval,那它们用来做动画的区别是什么呢?再一次借用视频中的截图解释一下:
每个区块看作是渲染的一帧,紫色和绿色是样式的计算、布局、绘制等(不一定每次渲染都有),总是在帧的开头也就是紫色前,每次页面渲染都会调用 rAF 的回调,同时保证每一帧只会调用一次 rAF。假设一帧周期是16.6ms,那么 setTimeout(callback, 0) 在一帧内就会调用大于一次,浏览器只渲染一次,因此这些多执行的 callback 是没有意义的。
因此,可以发现 rAF 是做流畅动画的最优选择。
rIC
rIC 也就是
requestIdleCallback。它会在在浏览器空闲情况下,一帧的最后执行,但是此时页面布局已经完成,所以不建议在requestIdleCallback里再操作 DOM,这样会导致页面再次重绘。所以可以把低优先级的任务放到空闲时间去执行,不要去影响延迟关键事件。
如果 requestIdleCallback 函数指定了 timeout。不管浏览器有多忙,会在指定的时间后强制执行 rIC 回调函数,这个机制可以防止我们的空闲任务被“饿死”,但是可能对性能产生负面影响。
rIC 的具体应用场景,可以参考文章 你应该知道的requestIdleCallback