深入理解浏览器的任务队列和渲染机制

815

刷新率 FPS(Frames Per Second)

现实场景中网页内容的每一帧我们都称做一次渲染,当衡量一个浏览器页面的性能标准时,通常会说页面在 60FPS 的帧率下是否流畅,60FPS 换算一下就是每一帧要在 16.7ms (1000/60) 内完成渲染。因此当页面每秒绘制小于 60 帧时就是所谓的丢帧,人眼就能感受到页面出现卡顿。但是浏览器不会让页面偶尔丢帧而会选择降低帧率,例如选择30FPS

补充:对于浏览器上下文不可见页面,帧率会降低到 4FPS 左右甚至更低。

单帧渲染流程 - 像素管道

通过 Google Developers 里的描述,用户拥有对帧性能最大控制权关键点为:

image.png

  • JavaScript:经常会在实际场景中使用 JavaScript 来实现一些视觉变化的效果。
  • 样式计算 Style :根据匹配选择器确定出最终 CSS 规则并应用到对应元素上。(选择器权重确定)
  • 布局 Layout :知道元素对应的最终 CSS 规则后,计算出占据屏幕大小以及位置,创建只包含可见元素的布局树。(修改了元素大小、位置等会引发重排 reflow
  • 绘制 Paint :对每个图层 Layer 进行绘制,生成绘制列表,涉及绘出文本、颜色、图像、边框和阴影。(修改了元素的背景颜色、阴影等会引发重绘 repaint
  • 合成 Composite :把每个图层的绘制列表按正确的顺序绘制到屏幕上。

浏览器事件循环与渲染机制

HTML5官方规范描述 EventLoop 的执行步骤 :

  1. 执行宏任务队列和微任务队列就不解释了。

  2. 进入Update the rendering阶段,这里有个rendering opportunity概念,浏览上下文渲染会根据屏幕刷新率、页面性能、页面是否在后台来确定是否需要渲染。而且渲染间隔通常是固定的。

    因此每轮事件循环不一定会让浏览器进行渲染。一帧内可能会经历多轮事件循环,也就意味着会执行多个宏任务(Task)。

  3. 如果不需要渲染,以下步骤(只列举常用的)也不会运行了:

    • 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,图片懒加载经常使用;
  4. 重新渲染用户界面。

  5. 判断宏任务队列或者微任务队列是否为空,如果为空则执行 Idle 空闲周期计算,判断是否需要执行 requestIdleCallback 的回调。

通过 Jake 演讲 EventLoop 中PPT里的图再来巩固一遍:

image.png image.png 图中白色方块转一圈就代表一轮事件循环,两个开关是浏览器的行为控制。

  • 左侧:任务队列;当有任务需要执行时,就加入 JS Stack 中进行执行。
  • 右侧:页面帧的渲染;浏览器会决定什么时候进行页面帧的渲染,需要渲染时才会打开开关,可以发现 rAF 的回调会在页面渲染前执行。

rAF

rAF 也就是 requestAnimationFrame,MDN 中对这个接口描述是:要求浏览器在下次重绘之前调用指定的回调函数更新动画。

实际场景中用rAF来做对比的基本也是setTimeoutsetInterval,那它们用来做动画的区别是什么呢?再一次借用视频中的截图解释一下:

image.png
每个区块看作是渲染的一帧,紫色和绿色是样式的计算、布局、绘制等(不一定每次渲染都有),总是在帧的开头也就是紫色前,每次页面渲染都会调用 rAF 的回调,同时保证每一帧只会调用一次 rAF。假设一帧周期是16.6ms,那么 setTimeout(callback, 0) 在一帧内就会调用大于一次,浏览器只渲染一次,因此这些多执行的 callback 是没有意义的。

因此,可以发现 rAF 是做流畅动画的最优选择。

rIC

rIC 也就是 requestIdleCallback它会在在浏览器空闲情况下,一帧的最后执行,但是此时页面布局已经完成,所以不建议在 requestIdleCallback 里再操作 DOM,这样会导致页面再次重绘。所以可以把低优先级的任务放到空闲时间去执行,不要去影响延迟关键事件。

如果 requestIdleCallback 函数指定了 timeout。不管浏览器有多忙,会在指定的时间后强制执行 rIC 回调函数,这个机制可以防止我们的空闲任务被“饿死”,但是可能对性能产生负面影响。

rIC 的具体应用场景,可以参考文章 你应该知道的requestIdleCallback