像素管道

image.png
黄色部分是脚本执行,紫色部分是更新render树、计算布局,绿色部分是绘制

  • JavaScript。一般来说,我们会使用JavaScript来实现一些视觉变化的效果,比如用jQuery的animation函数做一个动画,对一个数据集进行排序或者往页面里添加一些DOM元素等。当然,除了JavaScript,还有其他一些常用方法也可以实现视觉变化效果,比如:CSS Animations、Transitions和Web Animation API
  • 样式计算。此过程是根据匹配选择器(如.headline或.nav > .nav_item)计算出哪些元素应用哪些CSS规则的过程。从中知道规则之后,将应用规则并计算每个元素的最终样式。
  • 布局。在知道对一个元素应用哪些规则之后,浏览器即可开始计算它要占据的空间大小及其在屏幕的位置。网页的布局模式意味着一个元素可能影响其他元素,例如元素的宽度一般会影响其子元素的宽度以及树中各处的节点,因此对于浏览器来说,布局过程是经常发生的。
  • 绘制。绘制是填充像素的过程。它涉及汇出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分。绘制一般是在多个表面(通常称为层)上完成的。,
  • 合成。由于页面的各部分可能被绘制到多层,由此它们需要按正确顺序绘制到屏幕上,以便正确渲染页面。对于与另一元素重叠的元素来说,这点特别重要,因为一个错误可能使一个元素错误地出现在另一个元素的上层。

管道的每个部分都有机会产生卡顿,因此务必准确了解您的代码触发管道的哪些部分。
有时您可能听到与绘制一起使用的术语“栅格化”。这是因为绘制实际上分为两个任务:1)创建绘图调用的列表。2)填充像素。
后者称为“栅格化”,因此每当您在DevTools中看到绘制记录时,就应当将其视为包括栅格化。(在某些架构下,绘图调用的列表创建以及栅格化是在不同的线程中完成,但是这不是开发者所能控制的)
不一定每帧都总是会经过管道每个部分的处理。实际上,不管是使用JavaScript、CSS还是网络动画,在实现视觉变化时,管道针对指定帧的运行通常有三种方式:

  1. JS / CSS -> 样式 -> 布局 -> 绘制 -> 合成

image.png
如果您修改元素的“layout”属性,也就是改变了元素的几何属性(例如宽度、高度、左侧或顶部位置等),那么浏览器就必须检查所有其他元素,然后“自动重排”页面,任何受影响的部分都需要重新绘制,而且最终绘制的元素需进行合成。

  1. JS / CSS > 样式 > 绘制 > 合成

image.png
如果您修改“paint only”属性(例如背景图片、文字颜色或阴影等),即不会影响页面布局的属性,则浏览器会跳过布局,但仍将执行绘制。

  1. JS / CSS > 样式 > 合成

image.png

渲染更新的时机

如果有宏任务(eg:setTimeout),渲染的时机是在宏任务(eg:setTimeout)之后
渲染的时机是在微任务之后,也就是宏任务、微任务都是在重绘之前
UI渲染也是一个宏任务

卡死、卡顿

60fps,每秒钟刷新60次,一般看出来卡,页面会显得很流畅。平均16.67ms刷新一次
浏览器的渲染并不总是60fps, 60fps只是一个参考值

什么是卡死

fps = 1

卡顿

1 < fps < 60

setTimeout 和 requestAnimationFrame

一、主线程、js线程,ui渲染线程
二、requestAnimationFrame比setTimeout更精准,一般在ui渲染时,用requestAnimationFrame代替setTimeout
image.png
三、react源码,利用requestAnimationFrame做出声明周期,控制渲染流程
四、

  1. <script>
  2. var con = document.getElementById('con')
  3. con.onclick = function click1() {
  4. setTimeout(function setTimeout1() {
  5. con.innerHTML = 1
  6. }, 0)
  7. setTimeout(function setTimeout2() {
  8. con.innerHTML = 2
  9. }, 0)
  10. }
  11. </script>

当点击后,一共产生3个task,分别是click1、setTimeout1、setTimeout2,所以会分别在3次event loop中进行。
下面截取的是setTimeout1、setTimeout2的部分
image.png
我们修改了两次textContent,但是setTimeout1、setTimeout2之间没有paint,浏览器只绘制了textContent=1
五、加了微任务之后,掉帧的回来了。
找回正确的渲染过程,也属于渲染优化

  1. var con = document.getElementById('con')
  2. con.onclick = function() {
  3. setTimeout(function setTimeout1() {
  4. con.textContent = 0
  5. Promise.resolve().then(function Promise1() {
  6. console.log('promise1')
  7. })
  8. }, 0)
  9. setTimeout(function setTimeout2() {
  10. con.textContent = 1
  11. Promise.resolve().then(function Promise2() {
  12. console.log('promise2')
  13. })
  14. }, 0)
  15. }

image.png
从run microtasks中可以看出,setTimeout1、setTimeout2应该运行在两次event loop中,textContent = 0的修改被跳过了。
setTimeout1、setTimeout2的运行间隔很短,在setTimout1完成之后,setTimeout2马上就开始执行了,我们知道浏览器会尽量保持每秒60帧的刷新频率(大约16.67每帧),是不是只有两次event loop间隔大于16.67ms才会进行绘制呢?
六、requestAnimationFrame()
requestAnimationFrame()接收一个参数,在重绘屏幕前调用一个函数,告知浏览器在刷新下一帧时回调参数函数

  1. var con = document.getElementById('con')
  2. con.onclick = function() {
  3. setTimeout(function() {
  4. con.textContent = 0
  5. }, 0)
  6. setTimeout(function() {
  7. con.textContent = 1
  8. }, 0)
  9. setTimeout(function() {
  10. con.textContent = 2
  11. }, 0)
  12. setTimeout(function() {
  13. con.textContent = 3
  14. }, 0)
  15. setTimeout(function() {
  16. con.textContent = 4
  17. }, 0)
  18. setTimeout(function() {
  19. con.textContent = 5
  20. }, 0)
  21. setTimeout(function() {
  22. con.textContent = 6
  23. }, 0)
  24. }

image.png
图中一共绘制了两帧,第一帧4.4ms,第二帧9.3ms,都远远高于每秒60HZ(16.67ms)的频率,第一帧绘制的是con.textContent = 4,第二帧绘制的是con.textContent = 6。所以两次event loop的间隔时间很短同样会进行绘制。
七、有说法是一轮event loop执行的microtask有数量限制(可能是1000),多于的microtask会放到下一轮执行。下面将microtask的数量增加到25000
八、
查dom、浏览器api等的兼容性: https://caniuse.com/
requestAnimationFrame(),兼容性处理: https://github.com/facebook/react/blob/master/fixtures/dom/src/polyfills.js