刷新率 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