像素管道
黄色部分是脚本执行,紫色部分是更新render树、计算布局,绿色部分是绘制
- JavaScript。一般来说,我们会使用JavaScript来实现一些视觉变化的效果,比如用jQuery的animation函数做一个动画,对一个数据集进行排序或者往页面里添加一些DOM元素等。当然,除了JavaScript,还有其他一些常用方法也可以实现视觉变化效果,比如:CSS Animations、Transitions和Web Animation API
- 样式计算。此过程是根据匹配选择器(如.headline或.nav > .nav_item)计算出哪些元素应用哪些CSS规则的过程。从中知道规则之后,将应用规则并计算每个元素的最终样式。
- 布局。在知道对一个元素应用哪些规则之后,浏览器即可开始计算它要占据的空间大小及其在屏幕的位置。网页的布局模式意味着一个元素可能影响其他元素,例如元素的宽度一般会影响其子元素的宽度以及树中各处的节点,因此对于浏览器来说,布局过程是经常发生的。
- 绘制。绘制是填充像素的过程。它涉及汇出文本、颜色、图像、边框和阴影,基本上包括元素的每个可视部分。绘制一般是在多个表面(通常称为层)上完成的。,
- 合成。由于页面的各部分可能被绘制到多层,由此它们需要按正确顺序绘制到屏幕上,以便正确渲染页面。对于与另一元素重叠的元素来说,这点特别重要,因为一个错误可能使一个元素错误地出现在另一个元素的上层。
管道的每个部分都有机会产生卡顿,因此务必准确了解您的代码触发管道的哪些部分。
有时您可能听到与绘制一起使用的术语“栅格化”。这是因为绘制实际上分为两个任务:1)创建绘图调用的列表。2)填充像素。
后者称为“栅格化”,因此每当您在DevTools中看到绘制记录时,就应当将其视为包括栅格化。(在某些架构下,绘图调用的列表创建以及栅格化是在不同的线程中完成,但是这不是开发者所能控制的)
不一定每帧都总是会经过管道每个部分的处理。实际上,不管是使用JavaScript、CSS还是网络动画,在实现视觉变化时,管道针对指定帧的运行通常有三种方式:
- JS / CSS -> 样式 -> 布局 -> 绘制 -> 合成
如果您修改元素的“layout”属性,也就是改变了元素的几何属性(例如宽度、高度、左侧或顶部位置等),那么浏览器就必须检查所有其他元素,然后“自动重排”页面,任何受影响的部分都需要重新绘制,而且最终绘制的元素需进行合成。
- JS / CSS > 样式 > 绘制 > 合成
如果您修改“paint only”属性(例如背景图片、文字颜色或阴影等),即不会影响页面布局的属性,则浏览器会跳过布局,但仍将执行绘制。
- JS / CSS > 样式 > 合成
渲染更新的时机
如果有宏任务(eg:setTimeout),渲染的时机是在宏任务(eg:setTimeout)之后
渲染的时机是在微任务之后,也就是宏任务、微任务都是在重绘之前
UI渲染也是一个宏任务
卡死、卡顿
60fps,每秒钟刷新60次,一般看出来卡,页面会显得很流畅。平均16.67ms刷新一次
浏览器的渲染并不总是60fps, 60fps只是一个参考值
什么是卡死
卡顿
setTimeout 和 requestAnimationFrame
一、主线程、js线程,ui渲染线程
二、requestAnimationFrame比setTimeout更精准,一般在ui渲染时,用requestAnimationFrame代替setTimeout
三、react源码,利用requestAnimationFrame做出声明周期,控制渲染流程
四、
<script>
var con = document.getElementById('con')
con.onclick = function click1() {
setTimeout(function setTimeout1() {
con.innerHTML = 1
}, 0)
setTimeout(function setTimeout2() {
con.innerHTML = 2
}, 0)
}
</script>
当点击后,一共产生3个task,分别是click1、setTimeout1、setTimeout2,所以会分别在3次event loop中进行。
下面截取的是setTimeout1、setTimeout2的部分
我们修改了两次textContent,但是setTimeout1、setTimeout2之间没有paint,浏览器只绘制了textContent=1
五、加了微任务之后,掉帧的回来了。
找回正确的渲染过程,也属于渲染优化
var con = document.getElementById('con')
con.onclick = function() {
setTimeout(function setTimeout1() {
con.textContent = 0
Promise.resolve().then(function Promise1() {
console.log('promise1')
})
}, 0)
setTimeout(function setTimeout2() {
con.textContent = 1
Promise.resolve().then(function Promise2() {
console.log('promise2')
})
}, 0)
}
从run microtasks中可以看出,setTimeout1、setTimeout2应该运行在两次event loop中,textContent = 0的修改被跳过了。
setTimeout1、setTimeout2的运行间隔很短,在setTimout1完成之后,setTimeout2马上就开始执行了,我们知道浏览器会尽量保持每秒60帧的刷新频率(大约16.67每帧),是不是只有两次event loop间隔大于16.67ms才会进行绘制呢?
六、requestAnimationFrame()
requestAnimationFrame()接收一个参数,在重绘屏幕前调用一个函数,告知浏览器在刷新下一帧时回调参数函数
var con = document.getElementById('con')
con.onclick = function() {
setTimeout(function() {
con.textContent = 0
}, 0)
setTimeout(function() {
con.textContent = 1
}, 0)
setTimeout(function() {
con.textContent = 2
}, 0)
setTimeout(function() {
con.textContent = 3
}, 0)
setTimeout(function() {
con.textContent = 4
}, 0)
setTimeout(function() {
con.textContent = 5
}, 0)
setTimeout(function() {
con.textContent = 6
}, 0)
}
图中一共绘制了两帧,第一帧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