渲染主线程都在干什么
渲染主线程的task都安排了什么任务
-
还没拿到html的时候已经分配了任务给主线程,这个任务是先执行一下unload事件,然后在开始navigationStart发起导航任务
-
在请求过程中执行完了commitNavigationEnd任务后,就开始执行domloading了,渲染进程接收浏览器进程提交的HTML数据开始dom解析
-
在整个HTML解析的过程中也分配了performance.timing中关键时间点记录的任务
性能指标
- Main指标是记录了渲染主线程的执行记录
- Compositer指标记录了合成线程的执行记录
- Raster指标,光栅化线程池,用来让GPU执行光栅化的任务
- GPU指标,执行raster指标中的任务
- Chrome_ChildIOThread指标,记录用户输入事件,网络事件,设备相关等事件
- Network指标,页面中每个网络请求所消耗的时长
- Timings指标,用来记录关键的时间节点产生的数据信息FP,FCP,LCP等
- Frames指标,浏览器生成每帧的记录,
- Interactions指标,用来记录用户的交互操作
页面加载过程
- 导航阶段,该阶段主要从网络进程接收HTML响应头和HTML响应体
- 解析HTML阶段,该阶段主要是将接收到的HTML数据转换为DOM和CSSOM
- 生成可显示的位图阶段,该阶段主要是利用DOM和CSSOM,经过计算布局,生成层树(LayerTree)生成绘制列表(Paint)完成合成等操作,生成最终的图片
导航流程
用户发出URL请求到页面解析的过程,叫做导航
- 浏览器进程接收到用户的URL请求,浏览器进程便将该URL转发给网络进程
- 网络发起URL,解析完响应头数据,并将数据转发给浏览器进程
- 浏览器进程接收到网络进程的响应头数据后,发送提交导航CommitNavigation消息到渲染进程
- 渲染进程接收到提交导航的信息后,便开始接收HTML数据,接收数据的方式是直接和网络进程建立数据管道
- 最后渲染进程会想浏览器进程确认提交,这是告诉浏览器进程已经准备好接收和解析页面数据了
- 浏览器进程接收到渲染进程提交的消息后,便开始移除之前的旧文档,然后更新浏览器进程中的页面状态
页面准备
重定向:在header定义了重定向才会有这个过程,如果没有重定向,不会产生这个过程。
app cache:会先检查这个域名是否有缓存,如果有缓存就不需要DNS解析域名。这里的app是值应用程序application,不指手机app。
DNS解析:把域名解析成IP,如果直接用ip地址访问,不产生这个过程。
TCP连接:http协议是经过TCP来传输的,所以产生一个http请求就会有TCP connect,但是依赖于长连接,不会产生这个过程。
request header:请求头信息。
request body:请求体信息,比如get请求是没有请求体信息的,所以没有这个过程,这就是为什么把头跟体分开写的原因。
response header:响应头信息。
response body:响应体信息。
解析HTML结构
加载外部脚本和样式表文件:正常来说JS、css都是外部加载的,当然有不正常的人啊,比如我。
解析并执行脚本代码
构建与解析HTML DOM树:这个过程可以去了解下DOM树是怎样的就明白啦。
加载外部图片
页面加载完成,显示出来啦
1. 准备新页面耗时:fetchStart - navigationStart
2. 重定向时间:redirectEnd - redirectStart
3. App Cache时间:domainLookupStart - fetchStart
4. DNS解析时间:domainLookupEnd - domainLookupStart
5. TCP连接时间:connectEnd - connectStart
6. request时间:responseEnd - requestStart
7. 请求完毕到DOM树加载:domInteractive -responseEnd
8.构建与解析DOM树,加载资源时间:domCompleter -domInteractive
8. load时间:loadEventEnd - loadEventStart
9. 整个页面加载时间:loadEventEnd -navigationStart
10. 白屏时间:responseStart-navigationStart
let timing = performance.timing
// DNS 解析耗时
timing.domainLookupEnd - timing.domainLookupStart
// TCP 连接耗时
timing.connectEnd - timing.connectStart
// SSL 安全连接耗时
timing.connectEnd - timing.secureConnectionStart
// 网络请求耗时
timing.responseStart - timing.requestStart
// 数据传输耗时
timing.responseEnd - timing.responseStart
// DOM 解析耗时
timing.domInteractive - timing.responseEnd
// 资源加载耗时
timing.loadEventStart - timing.domContentLoadedEventEnd
/* 关键性能指标 */
// 首包时间
timing.responseStart - timing.domainLookupStart
// 白屏时间
timing.responseStart - timing.navigationStart
// 首次可交互时间
timing.domInteractive - timing.requestStart
// HTML 加载完成时间, 即 DOM Ready 时间
timing.domContentLoadedEventEnd - timing.navigationStart
// 页面完全加载时间
timing.loadEventStart - timing.navigationStart
导航过程中分配的任务
- Send request表示网络请求已发送
- Receive Response,表示已经接收到HTTP响应头
- 执行DOM事件,pagehide、visibilitychange 和 unload 等事件,如果你注册了这些事件的回调函数,那么这些回调函数会依次在该任务中被调用。
- Recive Data,其他事件处理完成之后,接下来接收HTML数据,Recive Data表示数据已经被接收,如果数据量大会存在多个Recive Data过程
- Finish Load,所有数据接收完成后执行,该过程表示网络请求已经完成
解析HTML数据阶段
- 解析HTML,遇到js,先编译js,执行js,执行全局代码时,v8会先构造annoymous过程,annoymous执行中执行js中的函数,由于修改了dom又强制执行了parseHTML过程又生成新的dom
- DOM生成完毕后,会触发相关的DOM事件
- DOM生成完毕后,计算样式表,这就是生成CSSOM的过程
生成可显示位图阶段
- 生成DOM和CSSOM之后,渲染主线程首先执行了一些dom事件,
- 执行布局,layout
- 然后更新层树(Layer Tree),这个过程对应Update LayerTree
- 有了层树之后,就需要为层树中的每一层准备绘制列表了,这个过程叫做paint
- 准备每层的绘制列表之后,就需要利用绘制列表生成相应的位图,这个过程叫做Composite Layers
- Composite Layers之后主线程任务完全交给合成线程来执行
首先主线程执行到 Composite Layers 过程之后,便会将绘制列表等信息提交给合成线程,合成线程的执行记录你可以通过 Compositor 指标来查看。合成线程维护了一个 Raster 线程池,线程池中的每个线程称为 Rasterize,用来执行光栅化操作,对应的任务就是 Rasterize Paint。当然光栅化操作并不是在 Rasterize 线程中直接执行的,而是在 GPU 进程中执行的,因此 Rasterize 线程需要和 GPU 线程保持通信。然后 GPU 生成图像,最终这些图层会被提交给浏览器进程,浏览器进程将其合成并最终显示在页面上。
微任务检查点
- 在执行全局js代码时,创建了anonymous执行环境,然后运行了微任务队列任务
<body>
<script>
let p = new Promise(function (resolve, reject) {
resolve("成功!");
});
p.then(function (successMessage) {
console.log("p! " + successMessage);
})
let p1 = new Promise(function (resolve, reject) {
resolve("成功!");
});
p1.then(function (successMessage) {
console.log("p1! " + successMessage);
})
</script>
</bod>
- 多个脚本执行微任务会影响解析时间
<body>
<script>
let p = new Promise(function (resolve, reject) {
resolve("成功!");
});
p.then(function (successMessage) {
console.log("p! " + successMessage);
})
</script>
<script>
let p1 = new Promise(function (resolve, reject) {
resolve("成功!");
});
p1.then(function (successMessage) {
console.log("p1! " + successMessage);
})
</script>
</body>
- 在同步任务之后,开始检查微任务
let p = new Promise(function (resolve, reject) {
resolve("成功!");
});
p.then(function (successMessage) {
console.log("p! " + successMessage);
})
for(let i = 0; i < 10000; i++){
console.log(i)
}
audit 性能指标
- 首次绘制(First Paint)渲染进程确认要渲染当前的请求后,渲染进程会创建一个空白页面,我们把创建空白页面的这个时间点称为FP
- 首次有效绘制(First Meaningfull Paint)使用FCP来代替,因此js是关键资源,关键资源会阻塞渲染,当页面绘制第一个元素时,这个时候称为FCP
- 首屏时间(Speed Index)就是指LCP,继续执行js脚本,当首屏内容完全绘制时,这个时间点称为LCP,首屏时间值越大,那么加载速度越慢
- 首次CPU空闲时间(First CPU Idle)也叫First Interactive表示页面达到最小化可交互时间,也就是说并不需要等到页面上所有元素可交互,只要可以堆大部分用户输入做出响应即可,缩短CPU空闲时间,就需要尽可能的加载完关键资源,尽可能快的渲染出首屏内容,因此和FCP优化是一样的
- 完全可交互时间(Time To Interactive)简称 TTI,它表示页面中所有元素都达到了可交互的时长。简单理解就这时候页面的内容已经完全显示出来了,所有的 JavaScript 事件已经注册完成,页面能够对用户的交互做出快速响应,通常满足响应速度在 50 毫秒以内。如果要解决 TTI 时间过久的问题,我们可以推迟执行一些和生成页面无关的 JavaScript 工作。
- 最大估计输入延时(Max Potential First Input Delay)这个指标是估计你的 Web 页面在加载最繁忙的阶段, 窗口中响应用户输入所需的时间,为了改善该指标,我们可以使用 WebWorker 来执行一些计算,从而释放主线程。另一个有用的措施是重构 CSS 选择器,以确保它们执行较少的计算。
页面生命周期
- 加载阶段,是指发出请求到渲染完整页面的过程,影响这个阶段的主要因素有网络和js脚本
- 交互阶段,页面加载完成到用户交互的整合过程,影响这个阶段的主要因素是js脚本
- 关闭阶段,主要是用户发出关闭指令后页面所做的一些清理操作
加载阶段
html,css,js在网页加载过程都会阻塞首次渲染,能阻塞网页首次渲染的资源称为关键资源
- 关键资源个数
- 关键资源大小
- 请求关键资源需要多少个RTT
如何减少关键资源的个数?一种方式是可以将 JavaScript 和 CSS 改成内联的形式,比如上图的 JavaScript 和 CSS,若都改成内联模式,那么关键资源的个数就由 3 个减少到了 1 个。另一种方式,如果 JavaScript 代码没有 DOM 或者 CSSOM 的操作,则可以改成 async 或者 defer 属性;同样对于 CSS,如果不是在构建页面之前加载的,则可以添加媒体取消阻止显现的标志。当 JavaScript 标签加上了 async 或者 defer、CSSlink 属性之前加上了取消阻止显现的标志后,它们就变成了非关键资源了。如何减少关键资源的大小?可以压缩 CSS 和 JavaScript 资源,移除 HTML、CSS、JavaScript 文件中一些注释内容,也可以通过前面讲的取消 CSS 或者 JavaScript 中关键资源的方式。如何减少关键资源 RTT 的次数?可以通过减少关键资源的个数和减少关键资源的大小搭配来实现。除此之外,还可以使用 CDN 来减少每次 RTT 时长。
交互阶段
js通过修改DOM或者CSSOM都会触发生成一个新的帧,css修改也会触发生成新的帧。如果在计算样式阶段发现有布局信息的修改,那么就会触发重排操作,然后触发后续渲染流水线的一系列操作,这个代价是非常大的。同样如果在计算样式阶段没有发现有布局信息的修改,只是修改了颜色一类的信息,那么就不会涉及到布局相关的调整,所以可以跳过布局阶段,直接进入绘制阶段,这个过程叫重绘。不过重绘阶段的代价也是不小的。还有另外一种情况,通过 CSS 实现一些变形、渐变、动画等特效,这是由 CSS 触发的,并且是在合成线程上执行的,这个过程称为合成。因为它不会触发重排或者重绘,而且合成操作本身的速度就非常快,所以执行合成是效率最高的方
-
减少js脚本执行时间,如果js函数执行时间过长,这就严重霸占了执行其他渲染任务的时间,将一次执行的函数分解为多个任务,使得每次执行的时间不要过久
-
避免强制同步布局,通过DOM接口执行添加元素或者删除元素操作后,需要重新计算样式和布局正常情况
<html>
<body>
<div id="mian_div">
<li id="time_li">time</li>
<li>geekbang</li>
</div>
<p id="demo">强制布局demo</p>
<button onclick="foo()">添加新元素</button>
<script>
function foo() {
let main_div = document.getElementById("mian_div")
let new_node = document.createElement("li")
let textnode = document.createTextNode("time.geekbang")
new_node.appendChild(textnode);
document.getElementById("mian_div").appendChild(new_node);
}
</script>
</body>
</html>
在当前任务,在执行完foo后执行了立即执行了重绘重排,这就是强制布局
function foo() {
let main_div = document.getElementById("mian_div")
let new_node = document.createElement("li")
let textnode = document.createTextNode("time.geekbang")
new_node.appendChild(textnode);
document.getElementById("mian_div").appendChild(new_node);
console.log(main_div.offsetHeight)
}
避免强制同步布局
function foo() {
let main_div = document.getElementById("mian_div")
//为了避免强制同步布局,在修改DOM之前查询相关值
console.log(main_div.offsetHeight)
let new_node = document.createElement("li")
let textnode = document.createTextNode("time.geekbang")
new_node.appendChild(textnode);
document.getElementById("mian_div").appendChild(new_node);
}
- 避免布局抖动,在一次js执行过程中,多次执行强制布局和抖动操作,尽量不要在修改dom结构时再去查询一些相关的值
function foo() {
let time_li = document.getElementById("time_li")
for (let i = 0; i < 100; i++) {
let main_div = document.getElementById("mian_div")
let new_node = document.createElement("li")
let textnode = document.createTextNode("time.geekbang")
new_node.appendChild(textnode);
new_node.offsetHeight = time_li.offsetHeight;
document.getElementById("mian_div").appendChild(new_node);
}
}
- 合理利用css合成动画
合成动画是直接在合成线程上执行的,这和在主线程上执行的布局、绘制等操作不同,如果主线程被 JavaScript 或者一些布局任务占用,CSS 动画依然能继续执行。所以要尽量利用好 CSS 合成动画,如果能让 CSS 处理动画,就尽量交给 CSS 来操作。另外,如果能提前知道对某个元素执行动画操作,那就最好将其标记为 will-change,这是告诉渲染引擎需要将该元素单独生成一个图层。
.slide {
will-change: transform;
}
- 避免频繁的垃圾回收
JavaScript 使用了自动垃圾回收机制,如果在一些函数中频繁创建临时对象,那么垃圾回收器也会频繁地去执行垃圾回收策略。这样当垃圾回收操作发生时,就会占用主线程,从而影响到其他任务的执行,严重的话还会让用户产生掉帧、不流畅的感觉。
- 频繁垃圾回收不仅霸占大量的主线程的时间,而且还占用大量的内存,严重阻塞了后续页面的渲染
function strToArray(str) {
let i = 0
const len = str.length
let arr = new Uint16Array(str.length)
for (; i < len; ++i) {
arr[i] = str.charCodeAt(i)
}
return arr;
}
function foo() {
let i = 0
let str = 'test V8 GC'
while (i++ < 1e5) {
strToArray(str);
}
}
foo()